22 Commits

Author SHA1 Message Date
Burak Kaan Köse
133f768af4 Remove commented code. 2025-02-03 19:15:54 +01:00
Burak Kaan Köse
380883a3d5 Remove smtp encoding constraint. 2025-02-03 19:13:56 +01:00
Burak Kaan Köse
662ce99f94 Fixed an issue where disabled folders causing an exception on forced sync. 2025-02-02 17:53:41 +01:00
Burak Kaan Köse
006404aa87 Fix icons for yahoo and apple. 2025-02-02 17:53:24 +01:00
Burak Kaan Köse
42070a2e5b merge main 2025-02-02 15:48:47 +01:00
Burak Kaan Köse
6fcfb7ff7b Remove ARM32. Upgrade server to .NET 9. 2025-02-02 15:34:42 +01:00
Burak Kaan Köse
616b56f4bd Server crash handlers. 2025-02-01 23:15:01 +01:00
Burak Kaan Köse
16272fa049 NRE on canceled token accounts during setup. 2025-02-01 20:43:55 +01:00
Burak Kaan Köse
3021850fa0 Fixing the build issue.... 2025-02-01 20:25:22 +01:00
Burak Kaan Köse
bb2dbe1457 Bump google sdk package. 2025-01-30 19:33:28 +01:00
Burak Kaan Köse
62fc21be7e Yahoo custom settings. 2025-01-29 18:55:54 +01:00
Burak Kaan Köse
50d0ebc6c8 Fixing remote highest mode seq checks for qresync and condstore synchronizers. 2025-01-29 16:12:10 +01:00
Burak Kaan Köse
ad900d596c Update mailkit to resolve qresync bug with iCloud. 2025-01-29 16:06:31 +01:00
Burak Kaan Köse
7d8da33f42 Delegating idle synchronizations to server to post-sync operations. 2025-01-28 23:57:09 +01:00
Burak Kaan Köse
b73eb3efcb Bumping some nugets. More on the imap synchronizers. 2025-01-28 22:56:19 +01:00
Burak Kaan Köse
b7b51ac4e6 Batching condstore downloads into 50, using SORT extension for searches if supported. 2025-01-25 00:58:40 +01:00
Burak Kaan Köse
bf1480705d Update privacy policy url. 2025-01-25 00:57:49 +01:00
Burak Kaan Köse
973ab1570d Support for killing synchronizers. 2025-01-25 00:00:10 +01:00
Burak Kaan Köse
20010e77ae iCloud special imap handling. 2025-01-21 23:57:58 +01:00
Burak Kaan Köse
05280dfd42 Adding iCloud and Yahoo as special IMAP handling scenario. 2025-01-19 23:52:27 +01:00
Burak Kaan Köse
e0f87f1374 IDLE implementation, imap synchronization strategies basics and condstore synchronization. 2025-01-19 20:35:41 +01:00
Burak Kaan Köse
46cbf443cf Fixing an issue where scrollviewer overrides a part of template in mail list. Adjusted zoomed out header grid's corner radius. 2025-01-19 20:35:12 +01:00
922 changed files with 44127 additions and 48521 deletions

View File

@@ -149,7 +149,7 @@ csharp_preferred_modifier_order = public,private,protected,internal,static,exter
# Code-block preferences # Code-block preferences
csharp_prefer_braces = true:silent csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = file_scoped:error csharp_style_namespace_declarations = block_scoped:silent
# Expression-level preferences # Expression-level preferences
csharp_prefer_simple_default_expression = true:suggestion csharp_prefer_simple_default_expression = true:suggestion
@@ -287,6 +287,4 @@ csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_utf8_string_literals = true:suggestion csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_prefer_readonly_struct = true:suggestion csharp_style_prefer_readonly_struct = true:suggestion
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent
csharp_style_prefer_primary_constructors = true:silent
csharp_prefer_system_threading_lock = true:suggestion

View File

@@ -1,7 +0,0 @@
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
<IsAotCompatible>true</IsAotCompatible>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
</Project>

View File

@@ -1,68 +0,0 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="ColorHashSharp" Version="1.0.0" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Labs.Uwp.Controls.MarkdownTextBlock" Version="0.1.250206-build.2040" />
<PackageVersion Include="CommunityToolkit.Labs.Uwp.DependencyPropertyGenerator" Version="0.1.250206-build.2040" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Uwp.Animations" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Behaviors" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Segmented" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.SettingsControls" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Sizers" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.TabbedCommandBar" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.TokenizingTextBox" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.Uwp.Controls.Primitives" Version="8.2.250129-preview2" />
<PackageVersion Include="EmailValidation" Version="1.3.0" />
<PackageVersion Include="gravatar-dotnet" Version="0.1.3" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.0" />
<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.13.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Graph" Version="5.75.0" />
<PackageVersion Include="Microsoft.Identity.Client" Version="4.70.1" />
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.70.1" />
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.70.1" />
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
<PackageVersion Include="Microsoft.UI.Xaml" Version="2.8.7" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Uwp.Managed" Version="3.0.0" />
<PackageVersion Include="MimeKit" Version="4.11.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.12.0" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="SqlKata" Version="4.0.1" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.4" />
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
<PackageVersion Include="Win2D.uwp" Version="1.28.2" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageVersion Include="Google.Apis.Auth" Version="1.69.0" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.69.0.3667" />
<PackageVersion Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageVersion Include="Google.Apis.PeopleService.v1" Version="1.68.0.3359" />
<PackageVersion Include="HtmlKit" Version="1.2.0" />
<PackageVersion Include="MailKit" Version="4.11.0" />
<PackageVersion Include="TimePeriodLibrary.NET" Version="2.1.6" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.4" />
</ItemGroup>
</Project>

View File

@@ -1,16 +1,17 @@
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Authentication; namespace Wino.Authentication
public abstract class BaseAuthenticator
{ {
public abstract MailProviderType ProviderType { get; } public abstract class BaseAuthenticator
protected IAuthenticatorConfig AuthenticatorConfig { get; }
protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
{ {
public abstract MailProviderType ProviderType { get; }
protected IAuthenticatorConfig AuthenticatorConfig { get; }
AuthenticatorConfig = authenticatorConfig; protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig)
{
AuthenticatorConfig = authenticatorConfig;
}
} }
} }

View File

@@ -7,44 +7,45 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication; using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication; namespace Wino.Authentication
public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
{ {
public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig) public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
{ {
} public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig)
public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
public bool ProposeCopyAuthURL { get; set; }
public override MailProviderType ProviderType => MailProviderType.Gmail;
/// <summary>
/// Generates the token information for the given account.
/// For gmail, interactivity is automatically handled when you get the token.
/// </summary>
/// <param name="account">Account to get token for.</param>
public Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
=> GetTokenInformationAsync(account);
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
{
var userCredential = await GetGoogleUserCredentialAsync(account);
if (userCredential.Token.IsStale)
{ {
await userCredential.RefreshTokenAsync(CancellationToken.None);
} }
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address); public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId;
} public bool ProposeCopyAuthURL { get; set; }
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account) public override MailProviderType ProviderType => MailProviderType.Gmail;
{
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets() /// <summary>
/// Generates the token information for the given account.
/// For gmail, interactivity is automatically handled when you get the token.
/// </summary>
/// <param name="account">Account to get token for.</param>
public Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
=> GetTokenInformationAsync(account);
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
{ {
ClientId = ClientId var userCredential = await GetGoogleUserCredentialAsync(account);
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
if (userCredential.Token.IsStale)
{
await userCredential.RefreshTokenAsync(CancellationToken.None);
}
return new TokenInformationEx(userCredential.Token.AccessToken, account.Address);
}
private Task<UserCredential> GetGoogleUserCredentialAsync(MailAccount account)
{
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
{
ClientId = ClientId
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
}
} }
} }

View File

@@ -11,116 +11,116 @@ using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authentication; using Wino.Core.Domain.Models.Authentication;
namespace Wino.Authentication; namespace Wino.Authentication
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
{ {
private const string TokenCacheFileName = "OutlookCache.bin"; public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
private bool isTokenCacheAttached = false;
// Outlook
private const string Authority = "https://login.microsoftonline.com/common";
public override MailProviderType ProviderType => MailProviderType.Outlook;
private readonly IPublicClientApplication _publicClientApplication;
private readonly IApplicationConfiguration _applicationConfiguration;
public OutlookAuthenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
{ {
_applicationConfiguration = applicationConfiguration; private const string TokenCacheFileName = "OutlookCache.bin";
private bool isTokenCacheAttached = false;
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri(); // Outlook
private const string Authority = "https://login.microsoftonline.com/common";
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows) public override MailProviderType ProviderType => MailProviderType.Outlook;
private readonly IPublicClientApplication _publicClientApplication;
private readonly IApplicationConfiguration _applicationConfiguration;
public OutlookAuthenticator(INativeAppService nativeAppService,
IApplicationConfiguration applicationConfiguration,
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
{ {
Title = "Wino Mail", _applicationConfiguration = applicationConfiguration;
ListOperatingSystemAccounts = true,
};
var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId) var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
.WithBroker(options)
.WithDefaultRedirectUri()
.WithAuthority(Authority);
_publicClientApplication = outlookAppBuilder.Build(); var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
} {
Title = "Wino Mail",
ListOperatingSystemAccounts = true,
};
public string[] Scope => AuthenticatorConfig.OutlookScope; var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
.WithBroker(options)
.WithDefaultRedirectUri()
.WithAuthority(Authority);
private async Task EnsureTokenCacheAttachedAsync() _publicClientApplication = outlookAppBuilder.Build();
{
if (!isTokenCacheAttached)
{
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
isTokenCacheAttached = true;
} }
}
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account) public string[] Scope => AuthenticatorConfig.OutlookScope;
{
await EnsureTokenCacheAttachedAsync();
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault( private async Task EnsureTokenCacheAttachedAsync()
a => string.Equals(a.Username?.Trim(), account.Address?.Trim(), StringComparison.OrdinalIgnoreCase));
if (storedAccount == null)
return await GenerateTokenInformationAsync(account);
try
{ {
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync(); if (!isTokenCacheAttached)
{
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username); isTokenCacheAttached = true;
}
} }
catch (MsalUiRequiredException)
{
// Somehow MSAL is not able to refresh the token silently.
// Force interactive login.
return await GenerateTokenInformationAsync(account); public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
}
catch (Exception)
{
throw;
}
}
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
{
try
{ {
await EnsureTokenCacheAttachedAsync(); await EnsureTokenCacheAttachedAsync();
var authResult = await _publicClientApplication var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address);
.AcquireTokenInteractive(Scope)
.ExecuteAsync();
// If the account is null, it means it's the initial creation of it. if (storedAccount == null)
// If not, make sure the authenticated user address matches the username. return await GenerateTokenInformationAsync(account);
// When people refresh their token, accounts must match.
if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase)) try
{ {
throw new AuthenticationException("Authenticated address does not match with your account address. If you are signing with a Office365, it is not officially supported yet."); var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
catch (MsalUiRequiredException)
{
// Somehow MSAL is not able to refresh the token silently.
// Force interactive login.
return await GenerateTokenInformationAsync(account);
}
catch (Exception)
{
throw;
}
}
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
{
try
{
await EnsureTokenCacheAttachedAsync();
var authResult = await _publicClientApplication
.AcquireTokenInteractive(Scope)
.ExecuteAsync();
// If the account is null, it means it's the initial creation of it.
// If not, make sure the authenticated user address matches the username.
// When people refresh their token, accounts must match.
if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase))
{
throw new AuthenticationException("Authenticated address does not match with your account address.");
}
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
catch (MsalClientException msalClientException)
{
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
throw new AccountSetupCanceledException();
throw;
} }
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username); throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
} }
catch (MsalClientException msalClientException)
{
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
throw new AccountSetupCanceledException();
throw;
}
throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
} }
} }

View File

@@ -1,23 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RootNamespace>Wino.Authentication</RootNamespace> <RootNamespace>Wino.Authentication</RootNamespace>
<Platforms>x86;x64;arm64</Platforms> <Configurations>Debug;Release</Configurations>
<LangVersion>12</LangVersion>
<Platforms>AnyCPU;x64;x86</Platforms>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio> <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" /> <PackageReference Include="CommunityToolkit.Diagnostics" Version="8.3.2" />
<PackageReference Include="CommunityToolkit.Mvvm" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.3.2" />
<PackageReference Include="Google.Apis.Auth" /> <PackageReference Include="Google.Apis.Auth" Version="1.68.0" />
<PackageReference Include="Microsoft.Identity.Client" /> <PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Broker" /> <PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" /> <PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.66.2" />
<PackageReference Include="Sentry.Serilog" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" /> <ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" /> <ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.0.0" />
<Properties>
<DisplayName>Wino Calendar</DisplayName>
<PublisherDisplayName>Burak KÖSE</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="10.0.14393.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Wino Calendar"
Description="Wino.Calendar.Packaging"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" />
<uap:SplashScreen Image="Images\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<!-- Registration of full trust backend application. -->
<uap:Extension Category="windows.appService">
<uap:AppService Name="WinoInteropService" />
</uap:Extension>
<!-- Protocol activation: Google OAuth -->
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="google.pw.oauth2">
<uap:DisplayName>Wino Google Authentication Protocol</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
<!-- Protocol activation: Launch UWP app from Full Trust Process -->
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="wino.calendar.launch">
<uap:DisplayName>Wino Calendara Launcher Protocol</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
<!-- Startup Task -->
<uap5:Extension
Category="windows.startupTask"
Executable="Wino.Server\Wino.Server.exe"
EntryPoint="Windows.FullTrustApplication">
<uap5:StartupTask
TaskId="WinoServer"
Enabled="false"
DisplayName="Wino Mail" />
</uap5:Extension>
<desktop:Extension Category="windows.fullTrustProcess" Executable="Wino.Server\Wino.Server.exe">
<desktop:FullTrustProcess>
<desktop:ParameterGroup GroupId="WinoServer" Parameters="Calendar" />
</desktop:FullTrustProcess>
</desktop:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="confirmAppClose" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
<VisualStudioVersion>15.0</VisualStudioVersion>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x86">
<Configuration>Debug</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x86">
<Configuration>Release</Configuration>
<Platform>x86</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|AnyCPU">
<Configuration>Debug</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|AnyCPU">
<Configuration>Release</Configuration>
<Platform>AnyCPU</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup>
<WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
</PropertyGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
<PropertyGroup>
<ProjectGuid>7485b18c-f5ab-4abe-ba7f-05b6623c67c8</ProjectGuid>
<TargetPlatformVersion>10.0.22621.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<DefaultLanguage>en-US</DefaultLanguage>
<AppxPackageSigningEnabled>false</AppxPackageSigningEnabled>
<NoWarn>$(NoWarn);NU1702</NoWarn>
<EntryPointProjectUniqueName>..\Wino.Calendar\Wino.Calendar.csproj</EntryPointProjectUniqueName>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
</PropertyGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup>
<Content Include="Images\SplashScreen.scale-200.png" />
<Content Include="Images\LockScreenLogo.scale-200.png" />
<Content Include="Images\Square150x150Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.scale-200.png" />
<Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Images\StoreLogo.png" />
<Content Include="Images\Wide310x150Logo.scale-200.png" />
<None Include="Package.StoreAssociation.xml" />
</ItemGroup>
<Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.1742" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Calendar\Wino.Calendar.csproj" />
<ProjectReference Include="..\Wino.Server\Wino.Server.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,37 +1,48 @@
using CommunityToolkit.Mvvm.Input; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Input;
using Wino.Calendar.ViewModels.Interfaces; using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.UI;
namespace Wino.Calendar.ViewModels; namespace Wino.Calendar.ViewModels
public partial class AccountDetailsPageViewModel : CalendarBaseViewModel
{ {
private readonly IAccountService _accountService; public partial class AccountDetailsPageViewModel : CalendarBaseViewModel
public AccountProviderDetailViewModel Account { get; private set; }
public ICalendarDialogService CalendarDialogService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public AccountDetailsPageViewModel(ICalendarDialogService calendarDialogService, IAccountService accountService, IAccountCalendarStateService accountCalendarStateService)
{ {
CalendarDialogService = calendarDialogService; private readonly IAccountService _accountService;
_accountService = accountService;
AccountCalendarStateService = accountCalendarStateService;
}
[RelayCommand] public AccountProviderDetailViewModel Account { get; private set; }
private void EditAccountDetails() public ICalendarDialogService CalendarDialogService { get; }
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsEditAccountDetails_Title, WinoPage.EditAccountDetailsPage, Account)); public IAccountCalendarStateService AccountCalendarStateService { get; }
public override void OnNavigatedTo(NavigationMode mode, object parameters) public AccountDetailsPageViewModel(ICalendarDialogService calendarDialogService, IAccountService accountService, IAccountCalendarStateService accountCalendarStateService)
{ {
base.OnNavigatedTo(mode, parameters); CalendarDialogService = calendarDialogService;
_accountService = accountService;
AccountCalendarStateService = accountCalendarStateService;
}
[RelayCommand]
private async Task RenameAccount()
{
if (Account == null)
return;
var updatedAccount = await CalendarDialogService.ShowEditAccountDialogAsync(Account.Account);
if (updatedAccount != null)
{
await _accountService.UpdateAccountAsync(updatedAccount);
ReportUIChange(new AccountUpdatedMessage(updatedAccount));
}
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
}
} }
} }

View File

@@ -13,134 +13,135 @@ using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Messaging.Server; using Wino.Messaging.Server;
namespace Wino.Calendar.ViewModels; namespace Wino.Calendar.ViewModels
public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
{ {
private readonly IProviderService _providerService; public partial class AccountManagementViewModel : AccountManagementPageViewModelBase
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; private readonly IProviderService _providerService;
_providerService = providerService;
}
public ICalendarDialogService CalendarDialogService { get; } public AccountManagementViewModel(ICalendarDialogService dialogService,
IWinoServerConnectionManager winoServerConnectionManager,
public override async void OnNavigatedTo(NavigationMode mode, object parameters) INavigationService navigationService,
{ IAccountService accountService,
base.OnNavigatedTo(mode, parameters); IProviderService providerService,
IStoreManagementService storeManagementService,
await InitializeAccountsAsync(); IAuthenticationProvider authenticationProvider,
} IPreferencesService preferencesService) : base(dialogService, winoServerConnectionManager, navigationService, accountService, providerService, storeManagementService, authenticationProvider, preferencesService)
public override async Task InitializeAccountsAsync()
{
Accounts.Clear();
var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
await ExecuteUIThread(() =>
{ {
foreach (var account in accounts) CalendarDialogService = dialogService;
{ _providerService = providerService;
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(); public ICalendarDialogService CalendarDialogService { get; }
var accountCreationDialogResult = await DialogService.ShowAccountProviderSelectionDialogAsync(availableProviders); public override async void OnNavigatedTo(NavigationMode mode, object parameters)
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, base.OnNavigatedTo(mode, parameters);
Name = accountCreationDialogResult.AccountName,
Id = Guid.NewGuid()
};
var tokenInformationResponse = await WinoServerConnectionManager await InitializeAccountsAsync();
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType, }
createdAccount,
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
if (accountCreationDialog.State == AccountCreationDialogState.Canceled) public override async Task InitializeAccountsAsync()
throw new AccountSetupCanceledException();
tokenInformationResponse.ThrowIfFailed();
await AccountService.CreateAccountAsync(createdAccount, null);
// Sync profile information if supported.
if (createdAccount.IsProfileInfoSyncSupported)
{ {
// Start profile information synchronization. Accounts.Clear();
// It's only available for Outlook and Gmail synchronizers.
var profileSyncOptions = new MailSynchronizationOptions() var accounts = await AccountService.GetAccountsAsync().ConfigureAwait(false);
await ExecuteUIThread(() =>
{ {
AccountId = createdAccount.Id, foreach (var account in accounts)
Type = MailSynchronizationType.UpdateProfile {
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);
accountCreationDialog.ShowDialog(accountCreationCancellationTokenSource);
accountCreationDialog.State = AccountCreationDialogState.SigningIn;
// 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 profileSynchronizationResponse = await WinoServerConnectionManager.GetResponseAsync<MailSynchronizationResult, NewMailSynchronizationRequested>(new NewMailSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client)); var tokenInformationResponse = await WinoServerConnectionManager
.GetResponseAsync<TokenInformationEx, AuthorizationRequested>(new AuthorizationRequested(accountCreationDialogResult.ProviderType,
createdAccount,
createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token);
var profileSynchronizationResult = profileSynchronizationResponse.Data; if (accountCreationDialog.State == AccountCreationDialogState.Canceled)
throw new AccountSetupCanceledException();
if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success) tokenInformationResponse.ThrowIfFailed();
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; await AccountService.CreateAccountAsync(createdAccount, null);
createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); // 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));
} }
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));
} }
} }

View File

@@ -21,346 +21,347 @@ using Wino.Messaging.Client.Calendar;
using Wino.Messaging.Client.Navigation; using Wino.Messaging.Client.Navigation;
using Wino.Messaging.Server; using Wino.Messaging.Server;
namespace Wino.Calendar.ViewModels; namespace Wino.Calendar.ViewModels
public partial class AppShellViewModel : CalendarBaseViewModel,
IRecipient<VisibleDateRangeChangedMessage>,
IRecipient<CalendarEnableStatusChangedMessage>,
IRecipient<NavigateManageAccountsRequested>,
IRecipient<CalendarDisplayTypeChangedMessage>,
IRecipient<DetailsPageStateChangedMessage>
{ {
public IPreferencesService PreferencesService { get; } public partial class AppShellViewModel : CalendarBaseViewModel,
public IStatePersistanceService StatePersistenceService { get; } IRecipient<VisibleDateRangeChangedMessage>,
public IAccountCalendarStateService AccountCalendarStateService { get; } IRecipient<CalendarEnableStatusChangedMessage>,
public INavigationService NavigationService { get; } IRecipient<NavigateManageAccountsRequested>,
public IWinoServerConnectionManager ServerConnectionManager { get; } IRecipient<CalendarDisplayTypeChangedMessage>,
IRecipient<DetailsPageStateChangedMessage>
[ObservableProperty]
private bool _isEventDetailsPageActive;
[ObservableProperty]
private int _selectedMenuItemIndex = -1;
[ObservableProperty]
private bool isCalendarEnabled;
/// <summary>
/// Gets or sets the active connection status of the Wino server.
/// </summary>
[ObservableProperty]
private WinoServerConnectionStatus activeConnectionStatus;
/// <summary>
/// Gets or sets the display date of the calendar.
/// </summary>
[ObservableProperty]
private DateTimeOffset _displayDate;
/// <summary>
/// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
/// </summary>
[ObservableProperty]
private DateRange highlightedDateRange;
[ObservableProperty]
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
[ObservableProperty]
private int _selectedDateNavigationHeaderIndex;
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
// For updating account calendars asynchronously.
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
public AppShellViewModel(IPreferencesService preferencesService,
IStatePersistanceService statePersistanceService,
IAccountService accountService,
ICalendarService calendarService,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService,
IWinoServerConnectionManager serverConnectionManager)
{ {
_accountService = accountService; public IPreferencesService PreferencesService { get; }
_calendarService = calendarService; public IStatePersistanceService StatePersistenceService { get; }
public IAccountCalendarStateService AccountCalendarStateService { get; }
public INavigationService NavigationService { get; }
public IWinoServerConnectionManager ServerConnectionManager { get; }
AccountCalendarStateService = accountCalendarStateService; [ObservableProperty]
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; private bool _isEventDetailsPageActive;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
NavigationService = navigationService; [ObservableProperty]
ServerConnectionManager = serverConnectionManager; private int _selectedMenuItemIndex = -1;
PreferencesService = preferencesService;
StatePersistenceService = statePersistanceService; [ObservableProperty]
StatePersistenceService.StatePropertyChanged += PrefefencesChanged; private bool isCalendarEnabled;
}
private void SelectedCalendarItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) /// <summary>
{ /// Gets or sets the active connection status of the Wino server.
throw new NotImplementedException(); /// </summary>
} [ObservableProperty]
private WinoServerConnectionStatus activeConnectionStatus;
private void PrefefencesChanged(object sender, string e) /// <summary>
{ /// Gets or sets the display date of the calendar.
if (e == nameof(StatePersistenceService.CalendarDisplayType)) /// </summary>
[ObservableProperty]
private DateTimeOffset _displayDate;
/// <summary>
/// Gets or sets the highlighted range in the CalendarView and displayed date range in FlipView.
/// </summary>
[ObservableProperty]
private DateRange highlightedDateRange;
[ObservableProperty]
private ObservableRangeCollection<string> dateNavigationHeaderItems = [];
[ObservableProperty]
private int _selectedDateNavigationHeaderIndex;
public bool IsVerticalCalendar => StatePersistenceService.CalendarDisplayType == CalendarDisplayType.Month;
// For updating account calendars asynchronously.
private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
public AppShellViewModel(IPreferencesService preferencesService,
IStatePersistanceService statePersistanceService,
IAccountService accountService,
ICalendarService calendarService,
IAccountCalendarStateService accountCalendarStateService,
INavigationService navigationService,
IWinoServerConnectionManager serverConnectionManager)
{ {
Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType)); _accountService = accountService;
_calendarService = calendarService;
AccountCalendarStateService = accountCalendarStateService;
AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested;
AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged;
// Change the calendar. NavigationService = navigationService;
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate())); ServerConnectionManager = serverConnectionManager;
PreferencesService = preferencesService;
StatePersistenceService = statePersistanceService;
StatePersistenceService.StatePropertyChanged += PrefefencesChanged;
} }
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters) private void SelectedCalendarItemsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
base.OnNavigatedTo(mode, parameters);
UpdateDateNavigationHeaderItems();
await InitializeAccountCalendarsAsync();
TodayClicked();
}
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{
// When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
// Reporting all these changes one by one to the UI is not efficient and may cause problems in the future.
// Update all calendar states at once.
try
{ {
await _accountCalendarUpdateSemaphoreSlim.WaitAsync(); throw new NotImplementedException();
}
foreach (var calendar in e.AccountCalendars) private void PrefefencesChanged(object sender, string e)
{
if (e == nameof(StatePersistenceService.CalendarDisplayType))
{ {
await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false); Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType));
// Change the calendar.
DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate()));
} }
} }
catch (Exception ex)
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{ {
Log.Error(ex, "Error while waiting for account calendar update semaphore."); base.OnNavigatedTo(mode, parameters);
UpdateDateNavigationHeaderItems();
await InitializeAccountCalendarsAsync();
TodayClicked();
} }
finally
private async void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e)
{ {
_accountCalendarUpdateSemaphoreSlim.Release(); // When using three-state checkbox, multiple accounts will be selected/unselected at the same time.
} // Reporting all these changes one by one to the UI is not efficient and may cause problems in the future.
}
private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) // Update all calendar states at once.
=> await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false); try
private async Task InitializeAccountCalendarsAsync()
{
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendar());
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var calendarViewModels = new List<AccountCalendarViewModel>();
foreach (var calendar in accountCalendars)
{ {
var calendarViewModel = new AccountCalendarViewModel(account, calendar); await _accountCalendarUpdateSemaphoreSlim.WaitAsync();
calendarViewModels.Add(calendarViewModel); foreach (var calendar in e.AccountCalendars)
{
await _calendarService.UpdateAccountCalendarAsync(calendar.AccountCalendar).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error while waiting for account calendar update semaphore.");
}
finally
{
_accountCalendarUpdateSemaphoreSlim.Release();
}
}
private async void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e)
=> await _calendarService.UpdateAccountCalendarAsync(e.AccountCalendar).ConfigureAwait(false);
private async Task InitializeAccountCalendarsAsync()
{
await Dispatcher.ExecuteOnUIThread(() => AccountCalendarStateService.ClearGroupedAccountCalendar());
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var calendarViewModels = new List<AccountCalendarViewModel>();
foreach (var calendar in accountCalendars)
{
var calendarViewModel = new AccountCalendarViewModel(account, calendar);
calendarViewModels.Add(calendarViewModel);
}
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
await Dispatcher.ExecuteOnUIThread(() =>
{
AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel);
});
}
}
private void ForceNavigateCalendarDate()
{
if (SelectedMenuItemIndex == -1)
{
var args = new CalendarPageNavigationArgs()
{
NavigationDate = _navigationDate ?? DateTime.Now.Date
};
// Already on calendar. Just navigate.
NavigationService.Navigate(WinoPage.CalendarPage, args);
_navigationDate = null;
}
else
{
SelectedMenuItemIndex = -1;
}
}
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue)
{
switch (newValue)
{
case -1:
ForceNavigateCalendarDate();
break;
case 0:
NavigationService.Navigate(WinoPage.ManageAccountsPage);
break;
case 1:
NavigationService.Navigate(WinoPage.SettingsPage);
break;
default:
break;
}
}
[RelayCommand]
private async Task Sync()
{
// Sync all calendars.
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
{
AccountId = account.Id,
Type = CalendarSynchronizationType.CalendarMetadata
}, SynchronizationSource.Client);
Messenger.Send(t);
}
}
/// <summary>
/// When calendar type switches, we need to navigate to the most ideal date.
/// This method returns that date.
/// </summary>
private DateTime GetDisplayTypeSwitchDate()
{
var settings = PreferencesService.GetCurrentCalendarSettings();
switch (StatePersistenceService.CalendarDisplayType)
{
case CalendarDisplayType.Day:
if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date;
return HighlightedDateRange.StartDate;
case CalendarDisplayType.Week:
if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now))
{
return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek);
}
return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek);
case CalendarDisplayType.WorkWeek:
break;
case CalendarDisplayType.Month:
break;
case CalendarDisplayType.Year:
break;
default:
break;
} }
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels); return DateTime.Today.Date;
}
await Dispatcher.ExecuteOnUIThread(() => private DateTime? _navigationDate;
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
#region Commands
[RelayCommand]
private void TodayClicked()
{
_navigationDate = DateTime.Now.Date;
ForceNavigateCalendarDate();
}
[RelayCommand]
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
[RelayCommand]
private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync();
[RelayCommand]
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
{
_navigationDate = clickedDateArgs.ClickedDate;
ForceNavigateCalendarDate();
}
#endregion
public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
/// <summary>
/// Sets the header navigation items based on visible date range and calendar type.
/// </summary>
private void UpdateDateNavigationHeaderItems()
{
DateNavigationHeaderItems.Clear();
// TODO: From settings
var testInfo = new CultureInfo("en-US");
switch (StatePersistenceService.CalendarDisplayType)
{ {
AccountCalendarStateService.AddGroupedAccountCalendar(groupedAccountCalendarViewModel); case CalendarDisplayType.Day:
case CalendarDisplayType.Week:
case CalendarDisplayType.WorkWeek:
case CalendarDisplayType.Month:
DateNavigationHeaderItems.ReplaceRange(testInfo.DateTimeFormat.MonthNames);
break;
case CalendarDisplayType.Year:
break;
default:
break;
}
SetDateNavigationHeaderItems();
}
partial void OnHighlightedDateRangeChanged(DateRange value) => SetDateNavigationHeaderItems();
private void SetDateNavigationHeaderItems()
{
if (HighlightedDateRange == null) return;
if (DateNavigationHeaderItems.Count == 0)
{
UpdateDateNavigationHeaderItems();
}
// TODO: Year view
var monthIndex = HighlightedDateRange.GetMostVisibleMonthIndex();
SelectedDateNavigationHeaderIndex = Math.Max(monthIndex - 1, -1);
}
public async void Receive(CalendarEnableStatusChangedMessage message)
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
public void Receive(CalendarDisplayTypeChangedMessage message) => OnPropertyChanged(nameof(IsVerticalCalendar));
public async void Receive(DetailsPageStateChangedMessage message)
{
await ExecuteUIThread(() =>
{
IsEventDetailsPageActive = message.IsActivated;
// TODO: This is for Wino Mail. Generalize this later on.
StatePersistenceService.IsReaderNarrowed = message.IsActivated;
StatePersistenceService.IsReadingMail = message.IsActivated;
}); });
} }
} }
private void ForceNavigateCalendarDate()
{
if (SelectedMenuItemIndex == -1)
{
var args = new CalendarPageNavigationArgs()
{
NavigationDate = _navigationDate ?? DateTime.Now.Date
};
// Already on calendar. Just navigate.
NavigationService.Navigate(WinoPage.CalendarPage, args);
_navigationDate = null;
}
else
{
SelectedMenuItemIndex = -1;
}
}
partial void OnSelectedMenuItemIndexChanged(int oldValue, int newValue)
{
switch (newValue)
{
case -1:
ForceNavigateCalendarDate();
break;
case 0:
NavigationService.Navigate(WinoPage.ManageAccountsPage);
break;
case 1:
NavigationService.Navigate(WinoPage.SettingsPage);
break;
default:
break;
}
}
[RelayCommand]
private async Task Sync()
{
// Sync all calendars.
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
foreach (var account in accounts)
{
var t = new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions()
{
AccountId = account.Id,
Type = CalendarSynchronizationType.CalendarMetadata
}, SynchronizationSource.Client);
Messenger.Send(t);
}
}
/// <summary>
/// When calendar type switches, we need to navigate to the most ideal date.
/// This method returns that date.
/// </summary>
private DateTime GetDisplayTypeSwitchDate()
{
var settings = PreferencesService.GetCurrentCalendarSettings();
switch (StatePersistenceService.CalendarDisplayType)
{
case CalendarDisplayType.Day:
if (HighlightedDateRange.IsInRange(DateTime.Now)) return DateTime.Now.Date;
return HighlightedDateRange.StartDate;
case CalendarDisplayType.Week:
if (HighlightedDateRange == null || HighlightedDateRange.IsInRange(DateTime.Now))
{
return DateTime.Now.Date.GetWeekStartDateForDate(settings.FirstDayOfWeek);
}
return HighlightedDateRange.StartDate.GetWeekStartDateForDate(settings.FirstDayOfWeek);
case CalendarDisplayType.WorkWeek:
break;
case CalendarDisplayType.Month:
break;
case CalendarDisplayType.Year:
break;
default:
break;
}
return DateTime.Today.Date;
}
private DateTime? _navigationDate;
private readonly IAccountService _accountService;
private readonly ICalendarService _calendarService;
#region Commands
[RelayCommand]
private void TodayClicked()
{
_navigationDate = DateTime.Now.Date;
ForceNavigateCalendarDate();
}
[RelayCommand]
public void ManageAccounts() => NavigationService.Navigate(WinoPage.AccountManagementPage);
[RelayCommand]
private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync();
[RelayCommand]
private void DateClicked(CalendarViewDayClickedEventArgs clickedDateArgs)
{
_navigationDate = clickedDateArgs.ClickedDate;
ForceNavigateCalendarDate();
}
#endregion
public void Receive(VisibleDateRangeChangedMessage message) => HighlightedDateRange = message.DateRange;
/// <summary>
/// Sets the header navigation items based on visible date range and calendar type.
/// </summary>
private void UpdateDateNavigationHeaderItems()
{
DateNavigationHeaderItems.Clear();
// TODO: From settings
var testInfo = new CultureInfo("en-US");
switch (StatePersistenceService.CalendarDisplayType)
{
case CalendarDisplayType.Day:
case CalendarDisplayType.Week:
case CalendarDisplayType.WorkWeek:
case CalendarDisplayType.Month:
DateNavigationHeaderItems.ReplaceRange(testInfo.DateTimeFormat.MonthNames);
break;
case CalendarDisplayType.Year:
break;
default:
break;
}
SetDateNavigationHeaderItems();
}
partial void OnHighlightedDateRangeChanged(DateRange value) => SetDateNavigationHeaderItems();
private void SetDateNavigationHeaderItems()
{
if (HighlightedDateRange == null) return;
if (DateNavigationHeaderItems.Count == 0)
{
UpdateDateNavigationHeaderItems();
}
// TODO: Year view
var monthIndex = HighlightedDateRange.GetMostVisibleMonthIndex();
SelectedDateNavigationHeaderIndex = Math.Max(monthIndex - 1, -1);
}
public async void Receive(CalendarEnableStatusChangedMessage message)
=> await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled);
public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1;
public void Receive(CalendarDisplayTypeChangedMessage message) => OnPropertyChanged(nameof(IsVerticalCalendar));
public async void Receive(DetailsPageStateChangedMessage message)
{
await ExecuteUIThread(() =>
{
IsEventDetailsPageActive = message.IsActivated;
// TODO: This is for Wino Mail. Generalize this later on.
StatePersistenceService.IsReaderNarrowed = message.IsActivated;
StatePersistenceService.IsReadingMail = message.IsActivated;
});
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -8,119 +8,120 @@ using Wino.Core.Domain.Translations;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels; namespace Wino.Calendar.ViewModels
public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
{ {
[ObservableProperty] public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
private double _cellHourHeight;
[ObservableProperty]
private int _selectedFirstDayOfWeekIndex;
[ObservableProperty]
private bool _is24HourHeaders;
[ObservableProperty]
private TimeSpan _workingHourStart;
[ObservableProperty]
private TimeSpan _workingHourEnd;
[ObservableProperty]
private List<string> _dayNames = [];
[ObservableProperty]
private int _workingDayStartIndex;
[ObservableProperty]
private int _workingDayEndIndex;
public IPreferencesService PreferencesService { get; }
private readonly bool _isLoaded = false;
public CalendarSettingsPageViewModel(IPreferencesService preferencesService)
{ {
PreferencesService = preferencesService; [ObservableProperty]
private double _cellHourHeight;
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage); [ObservableProperty]
private int _selectedFirstDayOfWeekIndex;
var cultureInfo = new CultureInfo(currentLanguageLanguageCode); [ObservableProperty]
private bool _is24HourHeaders;
// Populate the day names list [ObservableProperty]
for (var i = 0; i < 7; i++) private TimeSpan _workingHourStart;
[ObservableProperty]
private TimeSpan _workingHourEnd;
[ObservableProperty]
private List<string> _dayNames = [];
[ObservableProperty]
private int _workingDayStartIndex;
[ObservableProperty]
private int _workingDayEndIndex;
public IPreferencesService PreferencesService { get; }
private readonly bool _isLoaded = false;
public CalendarSettingsPageViewModel(IPreferencesService preferencesService)
{ {
_dayNames.Add(cultureInfo.DateTimeFormat.DayNames[i]); PreferencesService = preferencesService;
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
var cultureInfo = new CultureInfo(currentLanguageLanguageCode);
// Populate the day names list
for (var i = 0; i < 7; i++)
{
_dayNames.Add(cultureInfo.DateTimeFormat.DayNames[i]);
}
var cultureFirstDayName = cultureInfo.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek);
_selectedFirstDayOfWeekIndex = _dayNames.IndexOf(cultureFirstDayName);
_is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
_workingHourStart = preferencesService.WorkingHourStart;
_workingHourEnd = preferencesService.WorkingHourEnd;
_cellHourHeight = preferencesService.HourHeight;
_workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
_workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
_isLoaded = true;
} }
var cultureFirstDayName = cultureInfo.DateTimeFormat.GetDayName(preferencesService.FirstDayOfWeek); partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
partial void OnIs24HourHeadersChanged(bool value) => SaveSettings();
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
_selectedFirstDayOfWeekIndex = _dayNames.IndexOf(cultureFirstDayName); public void SaveSettings()
_is24HourHeaders = preferencesService.Prefer24HourTimeFormat;
_workingHourStart = preferencesService.WorkingHourStart;
_workingHourEnd = preferencesService.WorkingHourEnd;
_cellHourHeight = preferencesService.HourHeight;
_workingDayStartIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
_workingDayEndIndex = _dayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
_isLoaded = true;
}
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
partial void OnIs24HourHeadersChanged(bool value) => SaveSettings();
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
public void SaveSettings()
{
if (!_isLoaded) return;
PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
{ {
0 => DayOfWeek.Sunday, if (!_isLoaded) return;
1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException()
};
PreferencesService.WorkingDayStart = WorkingDayStartIndex switch PreferencesService.FirstDayOfWeek = SelectedFirstDayOfWeekIndex switch
{ {
0 => DayOfWeek.Sunday, 0 => DayOfWeek.Sunday,
1 => DayOfWeek.Monday, 1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday, 2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday, 3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday, 4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday, 5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday, 6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException()
}; };
PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch PreferencesService.WorkingDayStart = WorkingDayStartIndex switch
{ {
0 => DayOfWeek.Sunday, 0 => DayOfWeek.Sunday,
1 => DayOfWeek.Monday, 1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday, 2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday, 3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday, 4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday, 5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday, 6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException() _ => throw new ArgumentOutOfRangeException()
}; };
PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders; PreferencesService.WorkingDayEnd = WorkingDayEndIndex switch
PreferencesService.WorkingHourStart = WorkingHourStart; {
PreferencesService.WorkingHourEnd = WorkingHourEnd; 0 => DayOfWeek.Sunday,
PreferencesService.HourHeight = CellHourHeight; 1 => DayOfWeek.Monday,
2 => DayOfWeek.Tuesday,
3 => DayOfWeek.Wednesday,
4 => DayOfWeek.Thursday,
5 => DayOfWeek.Friday,
6 => DayOfWeek.Saturday,
_ => throw new ArgumentOutOfRangeException()
};
Messenger.Send(new CalendarSettingsUpdatedMessage()); PreferencesService.Prefer24HourTimeFormat = Is24HourHeaders;
PreferencesService.WorkingHourStart = WorkingHourStart;
PreferencesService.WorkingHourEnd = WorkingHourEnd;
PreferencesService.HourHeight = CellHourHeight;
Messenger.Send(new CalendarSettingsUpdatedMessage());
}
} }
} }

View File

@@ -1,12 +1,13 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Wino.Core; using Wino.Core;
namespace Wino.Calendar.ViewModels; namespace Wino.Calendar.ViewModels
public static class CalendarViewModelContainerSetup
{ {
public static void RegisterCalendarViewModelServices(this IServiceCollection services) public static class CalendarViewModelContainerSetup
{ {
services.RegisterCoreServices(); public static void RegisterCalendarViewModelServices(this IServiceCollection services)
{
services.RegisterCoreServices();
}
} }
} }

View File

@@ -4,66 +4,67 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data; namespace Wino.Calendar.ViewModels.Data
public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
{ {
public MailAccount Account { get; } public partial class AccountCalendarViewModel : ObservableObject, IAccountCalendar
public AccountCalendar AccountCalendar { get; }
public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
{ {
Account = account; public MailAccount Account { get; }
AccountCalendar = accountCalendar; public AccountCalendar AccountCalendar { get; }
IsChecked = accountCalendar.IsExtended; public AccountCalendarViewModel(MailAccount account, AccountCalendar accountCalendar)
{
Account = account;
AccountCalendar = accountCalendar;
IsChecked = accountCalendar.IsExtended;
}
[ObservableProperty]
private bool _isChecked;
partial void OnIsCheckedChanged(bool value) => IsExtended = value;
public string Name
{
get => AccountCalendar.Name;
set => SetProperty(AccountCalendar.Name, value, AccountCalendar, (u, n) => u.Name = n);
}
public string TextColorHex
{
get => AccountCalendar.TextColorHex;
set => SetProperty(AccountCalendar.TextColorHex, value, AccountCalendar, (u, t) => u.TextColorHex = t);
}
public string BackgroundColorHex
{
get => AccountCalendar.BackgroundColorHex;
set => SetProperty(AccountCalendar.BackgroundColorHex, value, AccountCalendar, (u, b) => u.BackgroundColorHex = b);
}
public bool IsExtended
{
get => AccountCalendar.IsExtended;
set => SetProperty(AccountCalendar.IsExtended, value, AccountCalendar, (u, i) => u.IsExtended = i);
}
public bool IsPrimary
{
get => AccountCalendar.IsPrimary;
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
}
public Guid AccountId
{
get => AccountCalendar.AccountId;
set => SetProperty(AccountCalendar.AccountId, value, AccountCalendar, (u, a) => u.AccountId = a);
}
public string RemoteCalendarId
{
get => AccountCalendar.RemoteCalendarId;
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
}
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
} }
[ObservableProperty]
private bool _isChecked;
partial void OnIsCheckedChanged(bool value) => IsExtended = value;
public string Name
{
get => AccountCalendar.Name;
set => SetProperty(AccountCalendar.Name, value, AccountCalendar, (u, n) => u.Name = n);
}
public string TextColorHex
{
get => AccountCalendar.TextColorHex;
set => SetProperty(AccountCalendar.TextColorHex, value, AccountCalendar, (u, t) => u.TextColorHex = t);
}
public string BackgroundColorHex
{
get => AccountCalendar.BackgroundColorHex;
set => SetProperty(AccountCalendar.BackgroundColorHex, value, AccountCalendar, (u, b) => u.BackgroundColorHex = b);
}
public bool IsExtended
{
get => AccountCalendar.IsExtended;
set => SetProperty(AccountCalendar.IsExtended, value, AccountCalendar, (u, i) => u.IsExtended = i);
}
public bool IsPrimary
{
get => AccountCalendar.IsPrimary;
set => SetProperty(AccountCalendar.IsPrimary, value, AccountCalendar, (u, i) => u.IsPrimary = i);
}
public Guid AccountId
{
get => AccountCalendar.AccountId;
set => SetProperty(AccountCalendar.AccountId, value, AccountCalendar, (u, a) => u.AccountId = a);
}
public string RemoteCalendarId
{
get => AccountCalendar.RemoteCalendarId;
set => SetProperty(AccountCalendar.RemoteCalendarId, value, AccountCalendar, (u, r) => u.RemoteCalendarId = r);
}
public Guid Id { get => ((IAccountCalendar)AccountCalendar).Id; set => ((IAccountCalendar)AccountCalendar).Id = value; }
} }

View File

@@ -5,41 +5,42 @@ using Itenso.TimePeriod;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.ViewModels.Data; namespace Wino.Calendar.ViewModels.Data
public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, ICalendarItemViewModel
{ {
public CalendarItem CalendarItem { get; } public partial class CalendarItemViewModel : ObservableObject, ICalendarItem, ICalendarItemViewModel
public string Title => CalendarItem.Title;
public Guid Id => CalendarItem.Id;
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
public DateTime EndDate => CalendarItem.EndDate;
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
public ITimePeriod Period => CalendarItem.Period;
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
[ObservableProperty]
private bool _isSelected;
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemViewModel(CalendarItem calendarItem)
{ {
CalendarItem = calendarItem; public CalendarItem CalendarItem { get; }
}
public override string ToString() => CalendarItem.Title; public string Title => CalendarItem.Title;
public Guid Id => CalendarItem.Id;
public IAccountCalendar AssignedCalendar => CalendarItem.AssignedCalendar;
public DateTime StartDate { get => CalendarItem.StartDate; set => CalendarItem.StartDate = value; }
public DateTime EndDate => CalendarItem.EndDate;
public double DurationInSeconds { get => CalendarItem.DurationInSeconds; set => CalendarItem.DurationInSeconds = value; }
public ITimePeriod Period => CalendarItem.Period;
public bool IsAllDayEvent => CalendarItem.IsAllDayEvent;
public bool IsMultiDayEvent => CalendarItem.IsMultiDayEvent;
public bool IsRecurringEvent => CalendarItem.IsRecurringEvent;
public bool IsRecurringChild => CalendarItem.IsRecurringChild;
public bool IsRecurringParent => CalendarItem.IsRecurringParent;
[ObservableProperty]
private bool _isSelected;
public ObservableCollection<CalendarEventAttendee> Attendees { get; } = new ObservableCollection<CalendarEventAttendee>();
public CalendarItemViewModel(CalendarItem calendarItem)
{
CalendarItem = calendarItem;
}
public override string ToString() => CalendarItem.Title;
}
} }

View File

@@ -6,140 +6,141 @@ using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.ViewModels.Data; namespace Wino.Calendar.ViewModels.Data
public partial class GroupedAccountCalendarViewModel : ObservableObject
{ {
public event EventHandler CollectiveSelectionStateChanged; public partial class GroupedAccountCalendarViewModel : ObservableObject
public event EventHandler<AccountCalendarViewModel> CalendarSelectionStateChanged;
public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
{ {
Account = account; public event EventHandler CollectiveSelectionStateChanged;
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels); public event EventHandler<AccountCalendarViewModel> CalendarSelectionStateChanged;
ManageIsCheckedState(); public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
foreach (var calendarViewModel in calendarViewModels) public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
{ {
calendarViewModel.PropertyChanged += CalendarPropertyChanged; Account = account;
} AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
AccountCalendars.CollectionChanged += CalendarListUpdated; ManageIsCheckedState();
}
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) foreach (var calendarViewModel in calendarViewModels)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (AccountCalendarViewModel calendar in e.NewItems)
{ {
calendar.PropertyChanged += CalendarPropertyChanged; calendarViewModel.PropertyChanged += CalendarPropertyChanged;
} }
AccountCalendars.CollectionChanged += CalendarListUpdated;
} }
else if (e.Action == NotifyCollectionChangedAction.Remove)
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{ {
foreach (AccountCalendarViewModel calendar in e.OldItems) if (e.Action == NotifyCollectionChangedAction.Add)
{ {
calendar.PropertyChanged -= CalendarPropertyChanged; foreach (AccountCalendarViewModel calendar in e.NewItems)
{
calendar.PropertyChanged += CalendarPropertyChanged;
}
} }
} else if (e.Action == NotifyCollectionChangedAction.Remove)
else if (e.Action == NotifyCollectionChangedAction.Reset)
{
foreach (AccountCalendarViewModel calendar in e.OldItems)
{ {
calendar.PropertyChanged -= CalendarPropertyChanged; foreach (AccountCalendarViewModel calendar in e.OldItems)
{
calendar.PropertyChanged -= CalendarPropertyChanged;
}
} }
} else if (e.Action == NotifyCollectionChangedAction.Reset)
}
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is AccountCalendarViewModel viewModel)
{
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
{ {
ManageIsCheckedState(); foreach (AccountCalendarViewModel calendar in e.OldItems)
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true); {
} calendar.PropertyChanged -= CalendarPropertyChanged;
} }
}
[ObservableProperty]
private bool _isExpanded = true;
[ObservableProperty]
private bool? isCheckedState = true;
private bool _isExternalPropChangeBlocked = false;
private void ManageIsCheckedState()
{
if (_isExternalPropChangeBlocked) return;
_isExternalPropChangeBlocked = true;
if (AccountCalendars.All(c => c.IsChecked))
{
IsCheckedState = true;
}
else if (AccountCalendars.All(c => !c.IsChecked))
{
IsCheckedState = false;
}
else
{
IsCheckedState = null;
}
_isExternalPropChangeBlocked = false;
}
partial void OnIsCheckedStateChanged(bool? newValue)
{
if (_isExternalPropChangeBlocked) return;
// Update is triggered by user on the three-state checkbox.
// We should not report all changes one by one.
_isExternalPropChangeBlocked = true;
if (newValue == null)
{
// Only primary calendars must be checked.
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
}
}
else
{
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, newValue.GetValueOrDefault());
} }
} }
_isExternalPropChangeBlocked = false; private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (sender is AccountCalendarViewModel viewModel)
{
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
{
ManageIsCheckedState();
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
}
}
}
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty); [ObservableProperty]
} private bool _isExpanded = true;
private void UpdateCalendarCheckedState(AccountCalendarViewModel accountCalendarViewModel, bool newValue, bool ignoreValueCheck = false) [ObservableProperty]
{ private bool? isCheckedState = true;
var currentValue = accountCalendarViewModel.IsChecked;
if (currentValue == newValue && !ignoreValueCheck) return; private bool _isExternalPropChangeBlocked = false;
accountCalendarViewModel.IsChecked = newValue; private void ManageIsCheckedState()
{
if (_isExternalPropChangeBlocked) return;
// No need to report. _isExternalPropChangeBlocked = true;
if (_isExternalPropChangeBlocked == true) return;
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel); if (AccountCalendars.All(c => c.IsChecked))
{
IsCheckedState = true;
}
else if (AccountCalendars.All(c => !c.IsChecked))
{
IsCheckedState = false;
}
else
{
IsCheckedState = null;
}
_isExternalPropChangeBlocked = false;
}
partial void OnIsCheckedStateChanged(bool? newValue)
{
if (_isExternalPropChangeBlocked) return;
// Update is triggered by user on the three-state checkbox.
// We should not report all changes one by one.
_isExternalPropChangeBlocked = true;
if (newValue == null)
{
// Only primary calendars must be checked.
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
}
}
else
{
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, newValue.GetValueOrDefault());
}
}
_isExternalPropChangeBlocked = false;
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
}
private void UpdateCalendarCheckedState(AccountCalendarViewModel accountCalendarViewModel, bool newValue, bool ignoreValueCheck = false)
{
var currentValue = accountCalendarViewModel.IsChecked;
if (currentValue == newValue && !ignoreValueCheck) return;
accountCalendarViewModel.IsChecked = newValue;
// No need to report.
if (_isExternalPropChangeBlocked == true) return;
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
}
} }
} }

View File

@@ -12,104 +12,105 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.ViewModels; namespace Wino.Calendar.ViewModels
public partial class EventDetailsPageViewModel : CalendarBaseViewModel
{ {
private readonly ICalendarService _calendarService; public partial class EventDetailsPageViewModel : CalendarBaseViewModel
private readonly INativeAppService _nativeAppService;
private readonly IPreferencesService _preferencesService;
public CalendarSettings CurrentSettings { get; }
#region Details
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
private CalendarItemViewModel _currentEvent;
[ObservableProperty]
private CalendarItemViewModel _seriesParent;
public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
#endregion
public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
{ {
_calendarService = calendarService; private readonly ICalendarService _calendarService;
_nativeAppService = nativeAppService; private readonly INativeAppService _nativeAppService;
_preferencesService = preferencesService; private readonly IPreferencesService _preferencesService;
CurrentSettings = _preferencesService.GetCurrentCalendarSettings(); public CalendarSettings CurrentSettings { get; }
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters) #region Details
{
base.OnNavigatedTo(mode, parameters);
Messenger.Send(new DetailsPageStateChangedMessage(true)); [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanViewSeries))]
private CalendarItemViewModel _currentEvent;
if (parameters == null || parameters is not CalendarItemTarget args) [ObservableProperty]
return; private CalendarItemViewModel _seriesParent;
await LoadCalendarItemTargetAsync(args); public bool CanViewSeries => CurrentEvent?.IsRecurringChild ?? false;
}
private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target) #endregion
{
try public EventDetailsPageViewModel(ICalendarService calendarService, INativeAppService nativeAppService, IPreferencesService preferencesService)
{ {
var currentEventItem = await _calendarService.GetCalendarItemTargetAsync(target); _calendarService = calendarService;
_nativeAppService = nativeAppService;
_preferencesService = preferencesService;
if (currentEventItem == null) CurrentSettings = _preferencesService.GetCurrentCalendarSettings();
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
Messenger.Send(new DetailsPageStateChangedMessage(true));
if (parameters == null || parameters is not CalendarItemTarget args)
return; return;
CurrentEvent = new CalendarItemViewModel(currentEventItem); await LoadCalendarItemTargetAsync(args);
}
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId); private async Task LoadCalendarItemTargetAsync(CalendarItemTarget target)
{
foreach (var item in attendees) try
{ {
CurrentEvent.Attendees.Add(item); var currentEventItem = await _calendarService.GetCalendarItemTargetAsync(target);
if (currentEventItem == null)
return;
CurrentEvent = new CalendarItemViewModel(currentEventItem);
var attendees = await _calendarService.GetAttendeesAsync(currentEventItem.EventTrackingId);
foreach (var item in attendees)
{
CurrentEvent.Attendees.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
} }
} }
catch (Exception ex)
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{ {
Debug.WriteLine(ex.Message); base.OnNavigatedFrom(mode, parameters);
Messenger.Send(new DetailsPageStateChangedMessage(false));
}
[RelayCommand]
private async Task SaveAsync()
{
}
[RelayCommand]
private async Task DeleteAsync()
{
}
[RelayCommand]
private Task JoinOnline()
{
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask;
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
}
[RelayCommand]
private async Task Respond(CalendarItemStatus status)
{
if (CurrentEvent == null) return;
} }
} }
public override void OnNavigatedFrom(NavigationMode mode, object parameters)
{
base.OnNavigatedFrom(mode, parameters);
Messenger.Send(new DetailsPageStateChangedMessage(false));
}
[RelayCommand]
private async Task SaveAsync()
{
}
[RelayCommand]
private async Task DeleteAsync()
{
}
[RelayCommand]
private Task JoinOnline()
{
if (CurrentEvent == null || string.IsNullOrEmpty(CurrentEvent.CalendarItem.HtmlLink)) return Task.CompletedTask;
return _nativeAppService.LaunchUriAsync(new Uri(CurrentEvent.CalendarItem.HtmlLink));
}
[RelayCommand]
private async Task Respond(CalendarItemStatus status)
{
if (CurrentEvent == null) return;
}
} }

View File

@@ -6,25 +6,26 @@ using System.Linq;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.ViewModels.Interfaces; namespace Wino.Calendar.ViewModels.Interfaces
public interface IAccountCalendarStateService : INotifyPropertyChanged
{ {
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; } public interface IAccountCalendarStateService : INotifyPropertyChanged
{
ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; }
event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged; event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged; event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar); public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar); public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar);
public void ClearGroupedAccountCalendar(); public void ClearGroupedAccountCalendar();
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar); public void AddAccountCalendar(AccountCalendarViewModel accountCalendar);
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar); public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar);
/// <summary> /// <summary>
/// Enumeration of currently selected calendars. /// Enumeration of currently selected calendars.
/// </summary> /// </summary>
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; } IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable { get; } IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable { get; }
}
} }

View File

@@ -1,13 +1,14 @@
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages; namespace Wino.Calendar.ViewModels.Messages
public class CalendarItemDoubleTappedMessage
{ {
public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel) public class CalendarItemDoubleTappedMessage
{ {
CalendarItemViewModel = calendarItemViewModel; public CalendarItemDoubleTappedMessage(CalendarItemViewModel calendarItemViewModel)
} {
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; } public CalendarItemViewModel CalendarItemViewModel { get; }
}
} }

View File

@@ -1,13 +1,14 @@
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.ViewModels.Messages; namespace Wino.Calendar.ViewModels.Messages
public class CalendarItemRightTappedMessage
{ {
public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel) public class CalendarItemRightTappedMessage
{ {
CalendarItemViewModel = calendarItemViewModel; public CalendarItemRightTappedMessage(CalendarItemViewModel calendarItemViewModel)
} {
CalendarItemViewModel = calendarItemViewModel;
}
public CalendarItemViewModel CalendarItemViewModel { get; } public CalendarItemViewModel CalendarItemViewModel { get; }
}
} }

View File

@@ -1,16 +1,17 @@
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.ViewModels.Messages; namespace Wino.Calendar.ViewModels.Messages
public class CalendarItemTappedMessage
{ {
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod) public class CalendarItemTappedMessage
{ {
CalendarItemViewModel = calendarItemViewModel; public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
ClickedPeriod = clickedPeriod; {
} CalendarItemViewModel = calendarItemViewModel;
ClickedPeriod = clickedPeriod;
}
public CalendarItemViewModel CalendarItemViewModel { get; } public CalendarItemViewModel CalendarItemViewModel { get; }
public CalendarDayModel ClickedPeriod { get; } public CalendarDayModel ClickedPeriod { get; }
}
} }

View File

@@ -1,13 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Platforms>x86;x64;arm64</Platforms> <LangVersion>12</LangVersion>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <Platforms>AnyCPU;x64;x86</Platforms>
<AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio> <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly> <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="TimePeriodLibrary.NET" /> <PackageReference Include="TimePeriodLibrary.NET" Version="2.1.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

308
Wino.Calendar.sln Normal file
View File

@@ -0,0 +1,308 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35424.110
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.Domain", "Wino.Core.Domain\Wino.Core.Domain.csproj", "{814400B6-5A05-4596-B451-3A116A147DC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.UWP", "Wino.Core.UWP\Wino.Core.UWP.csproj", "{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core.ViewModels", "Wino.Core.ViewModels\Wino.Core.ViewModels.csproj", "{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Messaging", "Wino.Messages\Wino.Messaging.csproj", "{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Server", "Wino.Server\Wino.Server.csproj", "{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Core", "Wino.Core\Wino.Core.csproj", "{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar", "Wino.Calendar\Wino.Calendar.csproj", "{600F4979-DB7E-409D-B7DA-B60BE4C55C35}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.SourceGenerators", "Wino.SourceGenerators\Wino.SourceGenerators.csproj", "{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}"
EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "Wino.Calendar.Packaging", "Wino.Calendar.Packaging\Wino.Calendar.Packaging.wapproj", "{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{CF850F8C-5042-4376-9CBA-C8F2BB554083}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Services", "Wino.Services\Wino.Services.csproj", "{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Authentication", "Wino.Authentication\Wino.Authentication.csproj", "{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM = Debug|ARM
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM = Release|ARM
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|ARM64.Build.0 = Debug|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x64.ActiveCfg = Debug|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x64.Build.0 = Debug|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x86.ActiveCfg = Debug|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Debug|x86.Build.0 = Debug|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|Any CPU.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM64.ActiveCfg = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|ARM64.Build.0 = Release|Any CPU
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x64.ActiveCfg = Release|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x64.Build.0 = Release|x64
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x86.ActiveCfg = Release|x86
{814400B6-5A05-4596-B451-3A116A147DC1}.Release|x86.Build.0 = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM.ActiveCfg = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM.Build.0 = Debug|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|ARM64.Build.0 = Debug|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.ActiveCfg = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x64.Build.0 = Debug|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.ActiveCfg = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Debug|x86.Build.0 = Debug|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|Any CPU.Build.0 = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM.ActiveCfg = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM.Build.0 = Release|Any CPU
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.ActiveCfg = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|ARM64.Build.0 = Release|ARM64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.ActiveCfg = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x64.Build.0 = Release|x64
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.ActiveCfg = Release|x86
{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}.Release|x86.Build.0 = Release|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|ARM64.Build.0 = Debug|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x64.ActiveCfg = Debug|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x64.Build.0 = Debug|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x86.ActiveCfg = Debug|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Debug|x86.Build.0 = Debug|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|Any CPU.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM64.ActiveCfg = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|ARM64.Build.0 = Release|Any CPU
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x64.ActiveCfg = Release|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x64.Build.0 = Release|x64
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x86.ActiveCfg = Release|x86
{510CD96C-B3FF-4EC9-A67B-845C842E6BEC}.Release|x86.Build.0 = Release|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|ARM64.Build.0 = Debug|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x64.ActiveCfg = Debug|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x64.Build.0 = Debug|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x86.ActiveCfg = Debug|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Debug|x86.Build.0 = Debug|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|Any CPU.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM64.ActiveCfg = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|ARM64.Build.0 = Release|Any CPU
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x64.ActiveCfg = Release|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x64.Build.0 = Release|x64
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x86.ActiveCfg = Release|x86
{AB588CFD-4B0C-4A1F-B711-1999E3D092D0}.Release|x86.Build.0 = Release|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|Any CPU.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|Any CPU.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|ARM64.Build.0 = Debug|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x64.ActiveCfg = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x64.Build.0 = Debug|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x86.ActiveCfg = Debug|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Debug|x86.Build.0 = Debug|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|Any CPU.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|Any CPU.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM64.ActiveCfg = Release|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|ARM64.Build.0 = Release|ARM64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x64.ActiveCfg = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x64.Build.0 = Release|x64
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x86.ActiveCfg = Release|x86
{92DA33FC-9252-40C5-BF71-67ACB0B56F2B}.Release|x86.Build.0 = Release|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|ARM64.Build.0 = Debug|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x64.ActiveCfg = Debug|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x64.Build.0 = Debug|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x86.ActiveCfg = Debug|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Debug|x86.Build.0 = Debug|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|Any CPU.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM64.ActiveCfg = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|ARM64.Build.0 = Release|Any CPU
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x64.ActiveCfg = Release|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x64.Build.0 = Release|x64
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x86.ActiveCfg = Release|x86
{87FFCBF4-DC17-4F09-90D6-102CF4C72BAF}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|Any CPU.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.ActiveCfg = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.Build.0 = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM.Deploy.0 = Debug|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.ActiveCfg = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Build.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|ARM64.Deploy.0 = Debug|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.ActiveCfg = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Build.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x64.Deploy.0 = Debug|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.ActiveCfg = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Build.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Debug|x86.Deploy.0 = Debug|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|Any CPU.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.ActiveCfg = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.Build.0 = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM.Deploy.0 = Release|ARM
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.ActiveCfg = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Build.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|ARM64.Deploy.0 = Release|ARM64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.ActiveCfg = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Build.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x64.Deploy.0 = Release|x64
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.ActiveCfg = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Build.0 = Release|x86
{600F4979-DB7E-409D-B7DA-B60BE4C55C35}.Release|x86.Deploy.0 = Release|x86
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|ARM64.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x64.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Debug|x86.Build.0 = Debug|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|Any CPU.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM64.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|ARM64.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x64.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x64.Build.0 = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x86.ActiveCfg = Release|Any CPU
{8A7EB697-D722-4E0F-B20E-9FC88373ADB5}.Release|x86.Build.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.ActiveCfg = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.Build.0 = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM.Deploy.0 = Debug|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Build.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|ARM64.Deploy.0 = Debug|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.ActiveCfg = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Build.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x64.Deploy.0 = Debug|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.ActiveCfg = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Build.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Debug|x86.Deploy.0 = Debug|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.Build.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|Any CPU.Deploy.0 = Release|Any CPU
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.ActiveCfg = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.Build.0 = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM.Deploy.0 = Release|ARM
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.ActiveCfg = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Build.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|ARM64.Deploy.0 = Release|ARM64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.ActiveCfg = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Build.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x64.Deploy.0 = Release|x64
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.ActiveCfg = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Build.0 = Release|x86
{7485B18C-F5AB-4ABE-BA7F-05B6623C67C8}.Release|x86.Deploy.0 = Release|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|ARM64.Build.0 = Debug|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x64.ActiveCfg = Debug|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x64.Build.0 = Debug|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x86.ActiveCfg = Debug|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Debug|x86.Build.0 = Debug|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|Any CPU.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM64.ActiveCfg = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|ARM64.Build.0 = Release|Any CPU
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x64.ActiveCfg = Release|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x64.Build.0 = Release|x64
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x86.ActiveCfg = Release|x86
{CF850F8C-5042-4376-9CBA-C8F2BB554083}.Release|x86.Build.0 = Release|x86
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|ARM64.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x64.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x64.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x86.ActiveCfg = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Debug|x86.Build.0 = Debug|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|Any CPU.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM64.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|ARM64.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x64.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x64.Build.0 = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x86.ActiveCfg = Release|Any CPU
{BBA49030-7277-48CF-B2FE-3D01CB6B6C81}.Release|x86.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|ARM64.Build.0 = Debug|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x64.ActiveCfg = Debug|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x64.Build.0 = Debug|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x86.ActiveCfg = Debug|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Debug|x86.Build.0 = Debug|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|Any CPU.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM64.ActiveCfg = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|ARM64.Build.0 = Release|Any CPU
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x64.ActiveCfg = Release|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x64.Build.0 = Release|x64
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x86.ActiveCfg = Release|x86
{16A979C2-F308-464F-9B2A-0AF8ED5EDB43}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -6,18 +6,19 @@ using Windows.UI.Xaml.Media.Animation;
using Wino.Activation; using Wino.Activation;
using Wino.Calendar.Views; using Wino.Calendar.Views;
namespace Wino.Calendar.Activation; namespace Wino.Calendar.Activation
public class DefaultActivationHandler : ActivationHandler<IActivatedEventArgs>
{ {
protected override Task HandleInternalAsync(IActivatedEventArgs args) public class DefaultActivationHandler : ActivationHandler<IActivatedEventArgs>
{ {
(Window.Current.Content as Frame).Navigate(typeof(AppShell), null, new DrillInNavigationTransitionInfo()); protected override Task HandleInternalAsync(IActivatedEventArgs args)
{
(Window.Current.Content as Frame).Navigate(typeof(AppShell), null, new DrillInNavigationTransitionInfo());
return Task.CompletedTask; return Task.CompletedTask;
}
// Only navigate if Frame content doesn't exist.
protected override bool CanHandleInternal(IActivatedEventArgs args)
=> (Window.Current?.Content as Frame)?.Content == null;
} }
// Only navigate if Frame content doesn't exist.
protected override bool CanHandleInternal(IActivatedEventArgs args)
=> (Window.Current?.Content as Frame)?.Content == null;
} }

View File

@@ -24,136 +24,141 @@ using Wino.Messaging.Client.Connection;
using Wino.Messaging.Server; using Wino.Messaging.Server;
using Wino.Services; using Wino.Services;
namespace Wino.Calendar; namespace Wino.Calendar
public sealed partial class App : WinoApplication, IRecipient<NewCalendarSynchronizationRequested>
{ {
private BackgroundTaskDeferral connectionBackgroundTaskDeferral; public sealed partial class App : WinoApplication, IRecipient<NewCalendarSynchronizationRequested>
public App()
{ {
InitializeComponent(); public override string AppCenterKey => "dfdad6ab-95f9-44cc-9112-45ec6730c49e";
WeakReferenceMessenger.Default.Register<NewCalendarSynchronizationRequested>(this);
}
public override IServiceProvider ConfigureServices() private BackgroundTaskDeferral connectionBackgroundTaskDeferral;
{ private BackgroundTaskDeferral toastActionBackgroundTaskDeferral;
var services = new ServiceCollection();
services.RegisterSharedServices(); public App()
services.RegisterCalendarViewModelServices();
services.RegisterCoreUWPServices();
services.RegisterCoreViewModels();
RegisterUWPServices(services);
RegisterViewModels(services);
RegisterActivationHandlers(services);
return services.BuildServiceProvider();
}
#region Dependency Injection
private void RegisterActivationHandlers(IServiceCollection services)
{
//services.AddTransient<ProtocolActivationHandler>();
//services.AddTransient<ToastNotificationActivationHandler>();
//services.AddTransient<FileActivationHandler>();
}
private void RegisterUWPServices(IServiceCollection services)
{
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<ICalendarDialogService, DialogService>();
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
services.AddTransient<IProviderService, ProviderService>();
services.AddSingleton<IAuthenticatorConfig, CalendarAuthenticatorConfig>();
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
}
private void RegisterViewModels(IServiceCollection services)
{
services.AddSingleton(typeof(AppShellViewModel));
services.AddSingleton(typeof(CalendarPageViewModel));
services.AddTransient(typeof(CalendarSettingsPageViewModel));
services.AddTransient(typeof(AccountManagementViewModel));
services.AddTransient(typeof(PersonalizationPageViewModel));
services.AddTransient(typeof(AccountDetailsPageViewModel));
services.AddTransient(typeof(EventDetailsPageViewModel));
}
#endregion
protected override void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
{
// TODO: Check server running.
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
LogActivation($"OnLaunched -> {args.GetType().Name}, Kind -> {args.Kind}, PreviousExecutionState -> {args.PreviousExecutionState}, IsPrelaunch -> {args.PrelaunchActivated}");
if (!args.PrelaunchActivated)
{ {
await ActivateWinoAsync(args); InitializeComponent();
WeakReferenceMessenger.Default.Register(this);
} }
}
protected override IEnumerable<ActivationHandler> GetActivationHandlers() public override IServiceProvider ConfigureServices()
{
return null;
}
protected override ActivationHandler<IActivatedEventArgs> GetDefaultActivationHandler()
=> new DefaultActivationHandler();
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails appServiceTriggerDetails)
{ {
LogActivation("OnBackgroundActivated -> AppServiceTriggerDetails received."); var services = new ServiceCollection();
// Only accept connections from callers in the same package services.RegisterSharedServices();
if (appServiceTriggerDetails.CallerPackageFamilyName == Package.Current.Id.FamilyName) services.RegisterCalendarViewModelServices();
services.RegisterCoreUWPServices();
services.RegisterCoreViewModels();
RegisterUWPServices(services);
RegisterViewModels(services);
RegisterActivationHandlers(services);
return services.BuildServiceProvider();
}
#region Dependency Injection
private void RegisterActivationHandlers(IServiceCollection services)
{
//services.AddTransient<ProtocolActivationHandler>();
//services.AddTransient<ToastNotificationActivationHandler>();
//services.AddTransient<FileActivationHandler>();
}
private void RegisterUWPServices(IServiceCollection services)
{
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<ICalendarDialogService, DialogService>();
services.AddTransient<ISettingsBuilderService, SettingsBuilderService>();
services.AddTransient<IProviderService, ProviderService>();
services.AddSingleton<IAuthenticatorConfig, CalendarAuthenticatorConfig>();
services.AddSingleton<IAccountCalendarStateService, AccountCalendarStateService>();
}
private void RegisterViewModels(IServiceCollection services)
{
services.AddSingleton(typeof(AppShellViewModel));
services.AddSingleton(typeof(CalendarPageViewModel));
services.AddTransient(typeof(CalendarSettingsPageViewModel));
services.AddTransient(typeof(AccountManagementViewModel));
services.AddTransient(typeof(PersonalizationPageViewModel));
services.AddTransient(typeof(AccountDetailsPageViewModel));
services.AddTransient(typeof(EventDetailsPageViewModel));
}
#endregion
protected override void OnApplicationCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e)
{
// TODO: Check server running.
}
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
LogActivation($"OnLaunched -> {args.GetType().Name}, Kind -> {args.Kind}, PreviousExecutionState -> {args.PreviousExecutionState}, IsPrelaunch -> {args.PrelaunchActivated}");
if (!args.PrelaunchActivated)
{ {
// Connection established from the fulltrust process await ActivateWinoAsync(args);
}
}
connectionBackgroundTaskDeferral = args.TaskInstance.GetDeferral(); protected override IEnumerable<ActivationHandler> GetActivationHandlers()
args.TaskInstance.Canceled += OnConnectionBackgroundTaskCanceled; {
return null;
}
AppServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection; protected override ActivationHandler<IActivatedEventArgs> GetDefaultActivationHandler()
=> new DefaultActivationHandler();
WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished()); protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails appServiceTriggerDetails)
{
LogActivation("OnBackgroundActivated -> AppServiceTriggerDetails received.");
// Only accept connections from callers in the same package
if (appServiceTriggerDetails.CallerPackageFamilyName == Package.Current.Id.FamilyName)
{
// Connection established from the fulltrust process
connectionBackgroundTaskDeferral = args.TaskInstance.GetDeferral();
args.TaskInstance.Canceled += OnConnectionBackgroundTaskCanceled;
AppServiceConnectionManager.Connection = appServiceTriggerDetails.AppServiceConnection;
WeakReferenceMessenger.Default.Send(new WinoServerConnectionEstablished());
}
}
}
public void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
sender.Canceled -= OnConnectionBackgroundTaskCanceled;
Log.Information($"Server connection background task was canceled. Reason: {reason}");
connectionBackgroundTaskDeferral?.Complete();
connectionBackgroundTaskDeferral = null;
AppServiceConnectionManager.Connection = null;
}
public async void Receive(NewCalendarSynchronizationRequested message)
{
try
{
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(message);
synchronizationResultResponse.ThrowIfFailed();
}
catch (WinoServerException serverException)
{
var dialogService = Services.GetService<ICalendarDialogService>();
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
} }
} }
} }
public void OnConnectionBackgroundTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
sender.Canceled -= OnConnectionBackgroundTaskCanceled;
Log.Information($"Server connection background task was canceled. Reason: {reason}");
connectionBackgroundTaskDeferral?.Complete();
connectionBackgroundTaskDeferral = null;
AppServiceConnectionManager.Connection = null;
}
public async void Receive(NewCalendarSynchronizationRequested message)
{
try
{
var synchronizationResultResponse = await AppServiceConnectionManager.GetResponseAsync<CalendarSynchronizationResult, NewCalendarSynchronizationRequested>(message);
synchronizationResultResponse.ThrowIfFailed();
}
catch (WinoServerException serverException)
{
var dialogService = Services.GetService<ICalendarDialogService>();
dialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, serverException.Message, InfoBarMessageType.Error);
}
}
} }

View File

@@ -1,40 +1,41 @@
using System; using System;
using Windows.Foundation; using Windows.Foundation;
namespace Wino.Calendar.Args; 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) /// <summary>
/// When a new timeline cell is selected.
/// </summary>
public class TimelineCellSelectedArgs : EventArgs
{ {
ClickedDate = clickedDate; public TimelineCellSelectedArgs(DateTime clickedDate, Point canvasPoint, Point positionerPoint, Size cellSize)
CanvasPoint = canvasPoint; {
PositionerPoint = positionerPoint; ClickedDate = clickedDate;
CellSize = cellSize; 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; }
} }
/// <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; }
} }

View File

@@ -1,8 +1,9 @@
using System; using System;
namespace Wino.Calendar.Args; namespace Wino.Calendar.Args
{
/// <summary> /// <summary>
/// When selected timeline cell is unselected. /// When selected timeline cell is unselected.
/// </summary> /// </summary>
public class TimelineCellUnselectedArgs : EventArgs { } public class TimelineCellUnselectedArgs : EventArgs { }
}

View File

@@ -2,29 +2,30 @@
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.Controls; 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 class CalendarItemCommandBarFlyout : CommandBarFlyout
public CalendarItemViewModel Item
{ {
get { return (CalendarItemViewModel)GetValue(ItemProperty); } public static readonly DependencyProperty ItemProperty = DependencyProperty.Register(nameof(Item), typeof(CalendarItemViewModel), typeof(CalendarItemCommandBarFlyout), new PropertyMetadata(null, new PropertyChangedCallback(OnItemChanged)));
set { SetValue(ItemProperty, value); }
}
public CalendarItemViewModel Item
private static void OnItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CalendarItemCommandBarFlyout flyout)
{ {
flyout.UpdateMenuItems(); 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()
{
} }
} }
private void UpdateMenuItems()
{
}
} }

View File

@@ -9,189 +9,190 @@ using Wino.Calendar.ViewModels.Messages;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public sealed partial class CalendarItemControl : UserControl
{ {
// Single tap has a delay to report double taps properly. public sealed partial class CalendarItemControl : UserControl
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); } // Single tap has a delay to report double taps properly.
set { SetValue(IsCustomEventAreaProperty, value); } private bool isSingleTap = false;
}
/// <summary> public static readonly DependencyProperty CalendarItemProperty = DependencyProperty.Register(nameof(CalendarItem), typeof(CalendarItemViewModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarItemChanged)));
/// Day that the calendar item is rendered at. public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register(nameof(IsDragging), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
/// It's needed for title manipulation and some other adjustments later on. public static readonly DependencyProperty IsCustomEventAreaProperty = DependencyProperty.Register(nameof(IsCustomEventArea), typeof(bool), typeof(CalendarItemControl), new PropertyMetadata(false));
/// </summary> public static readonly DependencyProperty CalendarItemTitleProperty = DependencyProperty.Register(nameof(CalendarItemTitle), typeof(string), typeof(CalendarItemControl), new PropertyMetadata(string.Empty));
public CalendarDayModel DisplayingDate public static readonly DependencyProperty DisplayingDateProperty = DependencyProperty.Register(nameof(DisplayingDate), typeof(CalendarDayModel), typeof(CalendarItemControl), new PropertyMetadata(null, new PropertyChangedCallback(OnDisplayDateChanged)));
{
get { return (CalendarDayModel)GetValue(DisplayingDateProperty); }
set { SetValue(DisplayingDateProperty, value); }
}
public string CalendarItemTitle /// <summary>
{ /// Whether the control is displaying as regular event or all-multi day area in the day control.
get { return (string)GetValue(CalendarItemTitleProperty); } /// </summary>
set { SetValue(CalendarItemTitleProperty, value); } public bool IsCustomEventArea
}
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(); get { return (bool)GetValue(IsCustomEventAreaProperty); }
set { SetValue(IsCustomEventAreaProperty, value); }
} }
}
private static void OnCalendarItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) /// <summary>
{ /// Day that the calendar item is rendered at.
if (d is CalendarItemControl control) /// It's needed for title manipulation and some other adjustments later on.
/// </summary>
public CalendarDayModel DisplayingDate
{ {
control.UpdateControlVisuals(); get { return (CalendarDayModel)GetValue(DisplayingDateProperty); }
set { SetValue(DisplayingDateProperty, value); }
} }
}
private void UpdateControlVisuals() public string CalendarItemTitle
{
// 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: get { return (string)GetValue(CalendarItemTitleProperty); }
// 1. All day events set { SetValue(CalendarItemTitleProperty, value); }
// 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); public CalendarItemViewModel CalendarItem
{
get { return (CalendarItemViewModel)GetValue(CalendarItemProperty); }
set { SetValue(CalendarItemProperty, value); }
}
if (periodRelation == Itenso.TimePeriod.PeriodRelation.StartInside || public bool IsDragging
periodRelation == PeriodRelation.EnclosingStartTouching) {
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)
{ {
// hour -> title control.UpdateControlVisuals();
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}"; 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 else
{ {
// Not expected, but there it is.
CalendarItemTitle = CalendarItem.Title; CalendarItemTitle = CalendarItem.Title;
} }
// Debug.WriteLine($"{CalendarItem.Title} Period relation with {DisplayingDate.Period.ToString()}: {periodRelation}"); UpdateVisualStates();
}
else
{
CalendarItemTitle = CalendarItem.Title;
} }
UpdateVisualStates(); private void UpdateVisualStates()
}
private void UpdateVisualStates()
{
if (CalendarItem == null) return;
if (CalendarItem.IsAllDayEvent)
{ {
VisualStateManager.GoToState(this, "AllDayEvent", true); if (CalendarItem == null) return;
}
else if (CalendarItem.IsMultiDayEvent) if (CalendarItem.IsAllDayEvent)
{
if (IsCustomEventArea)
{ {
VisualStateManager.GoToState(this, "CustomAreaMultiDayEvent", true); 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 else
{ {
// Hide it. VisualStateManager.GoToState(this, "RegularEvent", true);
VisualStateManager.GoToState(this, "MultiDayEvent", true);
} }
} }
else
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)
{ {
VisualStateManager.GoToState(this, "RegularEvent", true); if (CalendarItem == null) return;
isSingleTap = true;
await Task.Delay(100);
if (isSingleTap)
{
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, DisplayingDate));
}
} }
}
private void ControlDragStarting(UIElement sender, DragStartingEventArgs args) => IsDragging = true; private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
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)); if (CalendarItem == null) return;
isSingleTap = false;
WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem));
} }
}
private void ControlDoubleTapped(object sender, DoubleTappedRoutedEventArgs e) private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
{ {
if (CalendarItem == null) return; if (CalendarItem == null) return;
isSingleTap = false; WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem));
}
WeakReferenceMessenger.Default.Send(new CalendarItemDoubleTappedMessage(CalendarItem));
}
private void ControlRightTapped(object sender, RightTappedRoutedEventArgs e)
{
if (CalendarItem == null) return;
WeakReferenceMessenger.Default.Send(new CalendarItemRightTappedMessage(CalendarItem));
} }
} }

View File

@@ -1,42 +1,43 @@
using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Automation.Peers;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
namespace Wino.Calendar.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"; /// <summary>
private const string PART_NextButton = "NextButtonHorizontal"; /// FlipView that hides the navigation buttons and exposes methods to navigate to the next and previous items with animations.
/// </summary>
private Button PreviousButton; public class CustomCalendarFlipView : FlipView
private Button NextButton;
protected override void OnApplyTemplate()
{ {
base.OnApplyTemplate(); private const string PART_PreviousButton = "PreviousButtonHorizontal";
private const string PART_NextButton = "NextButtonHorizontal";
PreviousButton = GetTemplateChild(PART_PreviousButton) as Button; private Button PreviousButton;
NextButton = GetTemplateChild(PART_NextButton) as Button; private Button NextButton;
// Hide navigation buttons protected override void OnApplyTemplate()
PreviousButton.Opacity = NextButton.Opacity = 0; {
PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false; base.OnApplyTemplate();
var t = FindName("ScrollingHost"); PreviousButton = GetTemplateChild(PART_PreviousButton) as Button;
} NextButton = GetTemplateChild(PART_NextButton) as Button;
public void GoPreviousFlip() // Hide navigation buttons
{ PreviousButton.Opacity = NextButton.Opacity = 0;
var backPeer = new ButtonAutomationPeer(PreviousButton); PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false;
backPeer.Invoke();
}
public void GoNextFlip() var t = FindName("ScrollingHost");
{ }
var nextPeer = new ButtonAutomationPeer(NextButton);
nextPeer.Invoke(); public void GoPreviousFlip()
{
var backPeer = new ButtonAutomationPeer(PreviousButton);
backPeer.Invoke();
}
public void GoNextFlip()
{
var nextPeer = new ButtonAutomationPeer(NextButton);
nextPeer.Invoke();
}
} }
} }

View File

@@ -3,75 +3,76 @@ using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class DayColumnControl : Control
{ {
private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText); public class DayColumnControl : Control
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); } private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText);
set { SetValue(DayModelProperty, value); } private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder);
} private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText);
public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl);
public DayColumnControl() private const string TodayState = nameof(TodayState);
{ private const string NotTodayState = nameof(NotTodayState);
DefaultStyleKey = typeof(DayColumnControl);
}
protected override void OnApplyTemplate() private TextBlock HeaderDateDayText;
{ private TextBlock ColumnHeaderText;
base.OnApplyTemplate(); private Border IsTodayBorder;
private ItemsControl AllDayItemsControl;
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock; public CalendarDayModel DayModel
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(); get { return (CalendarDayModel)GetValue(DayModelProperty); }
} set { SetValue(DayModelProperty, value); }
}
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; public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date; public DayColumnControl()
{
DefaultStyleKey = typeof(DayColumnControl);
}
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false); protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
UpdateLayout(); 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();
}
} }
} }

View File

@@ -3,54 +3,55 @@ using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class DayHeaderControl : Control
{ {
private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock); public class DayHeaderControl : Control
private TextBlock HeaderTextblock;
public DayHeaderDisplayType DisplayType
{ {
get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); } private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
set { SetValue(DisplayTypeProperty, value); } private TextBlock HeaderTextblock;
}
public DateTime Date public DayHeaderDisplayType DisplayType
{
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(); get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
set { SetValue(DisplayTypeProperty, value); }
} }
}
private void UpdateHeaderText() public DateTime Date
{
if (HeaderTextblock != null)
{ {
HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm"); 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");
}
} }
} }
} }

View File

@@ -10,290 +10,291 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers; using Wino.Helpers;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class WinoCalendarControl : Control
{ {
private const string PART_WinoFlipView = nameof(PART_WinoFlipView); public class WinoCalendarControl : Control
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); } private const string PART_WinoFlipView = nameof(PART_WinoFlipView);
set { SetValue(DisplayTypeProperty, value); } private const string PART_IdleGrid = nameof(PART_IdleGrid);
}
public CalendarOrientation Orientation public event EventHandler<TimelineCellSelectedArgs> TimelineCellSelected;
{ public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
get { return (CalendarOrientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public ItemsPanelTemplate VerticalItemsPanelTemplate public event EventHandler ScrollPositionChanging;
{
get { return (ItemsPanelTemplate)GetValue(VerticalItemsPanelTemplateProperty); }
set { SetValue(VerticalItemsPanelTemplateProperty, value); }
}
public ItemsPanelTemplate HorizontalItemsPanelTemplate #region Dependency Properties
{
get { return (ItemsPanelTemplate)GetValue(HorizontalItemsPanelTemplateProperty); }
set { SetValue(HorizontalItemsPanelTemplateProperty, value); }
}
public DayRangeRenderModel SelectedFlipViewDayRange 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));
get { return (DayRangeRenderModel)GetValue(SelectedFlipViewDayRangeProperty); } public static readonly DependencyProperty SelectedFlipViewDayRangeProperty = DependencyProperty.Register(nameof(SelectedFlipViewDayRange), typeof(DayRangeRenderModel), typeof(WinoCalendarControl), new PropertyMetadata(null));
set { SetValue(SelectedFlipViewDayRangeProperty, value); } 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 ScrollViewer ActiveScrollViewer 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)));
get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); } public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(CalendarOrientation), typeof(WinoCalendarControl), new PropertyMetadata(CalendarOrientation.Horizontal, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated)));
set { SetValue(ActiveScrollViewerProperty, value); } public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day));
}
public WinoDayTimelineCanvas ActiveCanvas /// <summary>
{ /// Gets or sets the day-week-month-year display type.
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); } /// Orientation is not determined by this property, but Orientation property.
set { SetValue(ActiveCanvasProperty, value); } /// This property is used to determine the template to use for the calendar.
} /// </summary>
public CalendarDisplayType DisplayType
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(); get { return (CalendarDisplayType)GetValue(DisplayTypeProperty); }
set { SetValue(DisplayTypeProperty, value); }
} }
}
private static void OnIdleStateChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) public CalendarOrientation Orientation
{
if (calendar is WinoCalendarControl calendarControl)
{ {
calendarControl.UpdateIdleState(); get { return (CalendarOrientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
} }
}
public ItemsPanelTemplate VerticalItemsPanelTemplate
private static void OnActiveVerticalScrollViewerChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
{ {
if (e.OldValue is ScrollViewer oldScrollViewer) get { return (ItemsPanelTemplate)GetValue(VerticalItemsPanelTemplateProperty); }
{ set { SetValue(VerticalItemsPanelTemplateProperty, value); }
calendarControl.DeregisterScrollChanges(oldScrollViewer);
}
if (e.NewValue is ScrollViewer newScrollViewer)
{
calendarControl.RegisterScrollChanges(newScrollViewer);
}
calendarControl.ManageHighlightedDateRange();
} }
}
public ItemsPanelTemplate HorizontalItemsPanelTemplate
private static void OnActiveCanvasChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{
if (calendar is WinoCalendarControl calendarControl)
{ {
if (e.OldValue is WinoDayTimelineCanvas oldCanvas) get { return (ItemsPanelTemplate)GetValue(HorizontalItemsPanelTemplateProperty); }
{ set { SetValue(HorizontalItemsPanelTemplateProperty, value); }
// Dismiss any selection on the old canvas.
calendarControl.DeregisterCanvas(oldCanvas);
}
if (e.NewValue is WinoDayTimelineCanvas newCanvas)
{
calendarControl.RegisterCanvas(newCanvas);
}
calendarControl.ManageHighlightedDateRange();
} }
}
private void ManageCalendarOrientation() public DayRangeRenderModel SelectedFlipViewDayRange
{
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; get { return (DayRangeRenderModel)GetValue(SelectedFlipViewDayRangeProperty); }
double totalHeight = ActiveScrollViewer.ScrollableHeight; set { SetValue(SelectedFlipViewDayRangeProperty, value); }
double scrollPosition = timeSpan.TotalHours * hourHeght; }
ActiveScrollViewer.ChangeView(null, scrollPosition, null, disableAnimation: false); public ScrollViewer ActiveScrollViewer
}); {
} get { return (ScrollViewer)GetValue(ActiveScrollViewerProperty); }
public void ResetTimelineSelection() set { SetValue(ActiveScrollViewerProperty, value); }
{ }
if (ActiveCanvas == null) return;
ActiveCanvas.SelectedDateTime = null; public WinoDayTimelineCanvas ActiveCanvas
} {
get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
set { SetValue(ActiveCanvasProperty, value); }
}
public void GoNextRange() public bool IsFlipIdle
{ {
if (InternalFlipView == null) return; get { return (bool)GetValue(IsFlipIdleProperty); }
set { SetValue(IsFlipIdleProperty, value); }
}
InternalFlipView.GoNextFlip(); /// <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 void GoPreviousRange() public int SelectedFlipViewIndex
{ {
if (InternalFlipView == null) return; get { return (int)GetValue(SelectedFlipViewIndexProperty); }
set { SetValue(SelectedFlipViewIndexProperty, value); }
}
InternalFlipView.GoPreviousFlip(); #endregion
}
public void UnselectActiveTimelineCell() private WinoCalendarFlipView InternalFlipView;
{ private Grid IdleGrid;
if (ActiveCanvas == null) return;
ActiveCanvas.SelectedDateTime = null; public WinoCalendarControl()
} {
DefaultStyleKey = typeof(WinoCalendarControl);
SizeChanged += CalendarSizeChanged;
}
public CalendarItemControl GetCalendarItemControl(CalendarItemViewModel calendarItemViewModel) private static void OnCalendarOrientationPropertiesUpdated(DependencyObject calendar, DependencyPropertyChangedEventArgs e)
{ {
return this.FindDescendants<CalendarItemControl>().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel); 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);
}
} }
} }

View File

@@ -8,178 +8,179 @@ using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Collections; using Wino.Core.Domain.Collections;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls; 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 class WinoCalendarFlipView : CustomCalendarFlipView
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); } public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true));
set { SetValue(ActiveCanvasProperty, value); } 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> /// <summary>
/// Gets or sets the scroll viewer that is currently active in the flip view. /// Gets or sets the active canvas that is currently displayed in the flip view.
/// It's the vertical scroll that scrolls the timeline only, not the header part that belongs /// Each day-range of flip view item has a canvas that displays the day timeline.
/// to parent FlipView control. /// </summary>
/// </summary> public WinoDayTimelineCanvas ActiveCanvas
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(); get { return (WinoDayTimelineCanvas)GetValue(ActiveCanvasProperty); }
} set { SetValue(ActiveCanvasProperty, value); }
}
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; /// <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>
private void UpdateActiveScrollViewer() public ScrollViewer ActiveVerticalScrollViewer
{
if (SelectedIndex < 0)
ActiveVerticalScrollViewer = null;
else
{ {
GetCurrentFlipViewItem().ContinueWith(task => 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)
{ {
if (task.IsCompletedSuccessfully) flipView.RegisterItemsSourceChange();
{ }
var flipViewItem = task.Result;
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ActiveVerticalScrollViewer = flipViewItem.FindDescendant<ScrollViewer>();
});
}
});
} }
}
public void UpdateActiveCanvas() private static void OnSelectedIndexUpdated(DependencyObject d, DependencyProperty e)
{
if (SelectedIndex < 0)
ActiveCanvas = null;
else
{ {
GetCurrentFlipViewItem().ContinueWith(task => if (d is WinoCalendarFlipView flipView)
{ {
if (task.IsCompletedSuccessfully) flipView.UpdateActiveCanvas();
{ flipView.UpdateActiveScrollViewer();
var flipViewItem = task.Result; }
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ActiveCanvas = flipViewItem.FindDescendant<WinoDayTimelineCanvas>();
});
}
});
} }
}
/// <summary> private void RegisterItemsSourceChange()
/// 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. if (GetItemsSource() is INotifyCollectionChanged notifyCollectionChanged)
var dayRange = GetItemsSource()?.FirstOrDefault(a => a.CalendarDays.Any(b => b.RepresentingDate.Date == dateTime.Date));
if (dayRange != null)
{ {
var navigationItemIndex = GetItemsSource().IndexOf(dayRange); notifyCollectionChanged.CollectionChanged += ItemsSourceUpdated;
}
}
if (Math.Abs(navigationItemIndex - SelectedIndex) > 4) 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 =>
{ {
// Difference between dates are high. if (task.IsCompletedSuccessfully)
// 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) var flipViewItem = task.Result;
_ = Dispatcher.TryRunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{ {
GoPreviousFlip(); ActiveVerticalScrollViewer = flipViewItem.FindDescendant<ScrollViewer>();
} });
else }
});
}
}
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, () =>
{ {
GoNextFlip(); 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() private ObservableRangeCollection<DayRangeRenderModel> GetItemsSource()
=> ItemsSource as ObservableRangeCollection<DayRangeRenderModel>; => ItemsSource as ObservableRangeCollection<DayRangeRenderModel>;
}
} }

View File

@@ -11,283 +11,284 @@ using Wino.Calendar.Models;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class WinoCalendarPanel : Panel
{ {
private const double LastItemRightExtraMargin = 12d; public class WinoCalendarPanel : Panel
// 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); } private const double LastItemRightExtraMargin = 12d;
set { SetValue(PeriodProperty, value); }
}
public double HourHeight // Store each ICalendarItem measurements by their Id.
{ private readonly Dictionary<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
get { return (double)GetValue(HourHeightProperty); }
set { SetValue(HourHeightProperty, value); }
}
public Thickness EventItemMargin 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));
get { return (Thickness)GetValue(EventItemMarginProperty); } public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
set { SetValue(EventItemMarginProperty, value); }
}
private void ResetMeasurements() => _measurements.Clear(); public ITimePeriod Period
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. get { return (ITimePeriod)GetValue(PeriodProperty); }
// We can simply consider event must not have a top margin. set { SetValue(PeriodProperty, value); }
return 0d;
} }
double minutesFromStart = (childStart - Period.Start).TotalMinutes; public double HourHeight
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; get { return (double)GetValue(HourHeightProperty); }
} set { SetValue(HourHeightProperty, value); }
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; public Thickness EventItemMargin
}
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. get { return (Thickness)GetValue(EventItemMarginProperty); }
if (!(control.Content is ICalendarItem child)) continue; set { SetValue(EventItemMarginProperty, value); }
}
bool isHorizontallyLastItem = false; private void ResetMeasurements() => _measurements.Clear();
double childWidth = 0, private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
childHeight = Math.Max(0, GetChildHeight(child)), {
childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)), var childStart = calendarItemViewModel.StartDate;
childLeft = 0;
// No need to measure anything here. if (childStart <= Period.Start)
if (childHeight == 0) continue;
if (!_measurements.ContainsKey(child))
{ {
// Multi-day event. // 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.
childLeft = 0; return 0d;
childWidth = availableWidth; }
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 else
{ {
var childMeasurement = _measurements[child]; // Multi-day event.
// Check how many of the event falls into the current period.
childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width)); childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
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. return (childDurationInMinutes / 1440) * availableHeight;
double extraRightMargin = 0; }
// Multi-day events don't have any margin and their hit test is disabled. protected override Size MeasureOverride(Size availableSize)
if (!child.IsMultiDayEvent) {
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)
{ {
// Max of 5% of the width or 20px max. // We can't arrange this child.
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0; 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}");
} }
if (childWidth < 0) childWidth = 1;
// Regular events must have 2px margin return finalSize;
if (!child.IsMultiDayEvent && !child.IsAllDayEvent) }
#region ColumSpanning and Packing Algorithm
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
{
if (_measurements.ContainsKey(calendarItem))
{ {
childLeft += 2; _measurements[calendarItem] = measurement;
childTop += 2; }
childHeight -= 2; else
childWidth -= 2; {
_measurements.Add(calendarItem, measurement);
} }
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}");
} }
// Pick the left and right positions of each event, such that there are no overlap.
return finalSize; private void LayoutEvents(IEnumerable<ICalendarItem> events)
}
#region ColumSpanning and Packing Algorithm
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
{
if (_measurements.ContainsKey(calendarItem))
{ {
_measurements[calendarItem] = measurement; var columns = new List<List<ICalendarItem>>();
} DateTime? lastEventEnding = null;
else
{
_measurements.Add(calendarItem, measurement);
}
}
// Pick the left and right positions of each event, such that there are no overlap. foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
private void LayoutEvents(IEnumerable<ICalendarItem> events) {
{ // Multi-day events are not measured.
var columns = new List<List<ICalendarItem>>(); if (ev.IsMultiDayEvent) continue;
DateTime? lastEventEnding = null;
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate)) if (ev.Period.Start >= lastEventEnding)
{ {
// Multi-day events are not measured. PackEvents(columns);
if (ev.IsMultiDayEvent) continue; columns.Clear();
lastEventEnding = null;
}
if (ev.Period.Start >= lastEventEnding) 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); PackEvents(columns);
columns.Clear();
lastEventEnding = null;
} }
}
bool placed = false; // 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 col in columns)
{ {
if (!col.Last().Period.OverlapsWith(ev.Period)) foreach (var ev in col)
{ {
col.Add(ev); int colSpan = ExpandEvent(ev, iColumn, columns);
placed = true;
break; var leftWeight = iColumn / numColumns;
var rightWeight = (iColumn + colSpan) / numColumns;
AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
} }
}
if (!placed) iColumn++;
{
columns.Add(new List<ICalendarItem> { ev });
}
if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
{
lastEventEnding = ev.Period.End;
} }
} }
if (columns.Count > 0)
// 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)
{ {
PackEvents(columns); int colSpan = 1;
}
}
// Set the left and right positions for each event in the connected group. foreach (var col in columns.Skip(iColumn + 1))
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); foreach (var ev1 in col)
{
if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
}
var leftWeight = iColumn / numColumns; colSpan++;
var rightWeight = (iColumn + colSpan) / numColumns;
AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
} }
iColumn++; return colSpan;
}
}
// 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
} }
#endregion
} }

View File

@@ -4,88 +4,89 @@ using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class WinoCalendarTypeSelectorControl : Control
{ {
private const string PART_TodayButton = nameof(PART_TodayButton); public class WinoCalendarTypeSelectorControl : Control
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); } private const string PART_TodayButton = nameof(PART_TodayButton);
set { SetValue(TodayClickedCommandProperty, value); } 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 CalendarDisplayType SelectedType 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));
get { return (CalendarDisplayType)GetValue(SelectedTypeProperty); } public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null));
set { SetValue(SelectedTypeProperty, value); }
}
public int DisplayDayCount public ICommand TodayClickedCommand
{ {
get { return (int)GetValue(DisplayDayCountProperty); } get { return (ICommand)GetValue(TodayClickedCommandProperty); }
set { SetValue(DisplayDayCountProperty, value); } set { SetValue(TodayClickedCommandProperty, value); }
} }
private AppBarButton _todayButton; public CalendarDisplayType SelectedType
private AppBarToggleButton _dayToggle; {
private AppBarToggleButton _weekToggle; get { return (CalendarDisplayType)GetValue(SelectedTypeProperty); }
private AppBarToggleButton _monthToggle; set { SetValue(SelectedTypeProperty, value); }
private AppBarToggleButton _yearToggle; }
public WinoCalendarTypeSelectorControl() public int DisplayDayCount
{ {
DefaultStyleKey = typeof(WinoCalendarTypeSelectorControl); get { return (int)GetValue(DisplayDayCountProperty); }
} set { SetValue(DisplayDayCountProperty, value); }
}
protected override void OnApplyTemplate() private AppBarButton _todayButton;
{ private AppBarToggleButton _dayToggle;
base.OnApplyTemplate(); private AppBarToggleButton _weekToggle;
private AppBarToggleButton _monthToggle;
private AppBarToggleButton _yearToggle;
_todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton; public WinoCalendarTypeSelectorControl()
_dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton; {
_weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton; DefaultStyleKey = typeof(WinoCalendarTypeSelectorControl);
_monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton; }
_yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton;
Guard.IsNotNull(_todayButton, nameof(_todayButton)); protected override void OnApplyTemplate()
Guard.IsNotNull(_dayToggle, nameof(_dayToggle)); {
Guard.IsNotNull(_weekToggle, nameof(_weekToggle)); base.OnApplyTemplate();
Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
Guard.IsNotNull(_yearToggle, nameof(_yearToggle));
_todayButton.Click += TodayClicked; _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;
_dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); }; Guard.IsNotNull(_todayButton, nameof(_todayButton));
_weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); }; Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
_monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); }; Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
_yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); }; Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
Guard.IsNotNull(_yearToggle, nameof(_yearToggle));
UpdateToggleButtonStates(); _todayButton.Click += TodayClicked;
}
private void TodayClicked(object sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null); _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); };
private void SetSelectedType(CalendarDisplayType type) UpdateToggleButtonStates();
{ }
SelectedType = type;
UpdateToggleButtonStates();
}
private void UpdateToggleButtonStates() private void TodayClicked(object sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
{
_dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day; private void SetSelectedType(CalendarDisplayType type)
_weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week; {
_monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month; SelectedType = type;
_yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year; UpdateToggleButtonStates();
}
private void UpdateToggleButtonStates()
{
_dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
_weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
_monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
_yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year;
}
} }
} }

View File

@@ -8,139 +8,140 @@ using Windows.UI.Xaml.Media;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers; using Wino.Helpers;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class WinoCalendarView : Control
{ {
private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder); public class WinoCalendarView : Control
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); } private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder);
set { SetValue(TodayBackgroundColorProperty, value); } private const string PART_CalendarView = nameof(PART_CalendarView);
}
/// <summary> public static readonly DependencyProperty HighlightedDateRangeProperty = DependencyProperty.Register(nameof(HighlightedDateRange), typeof(DateRange), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnHighlightedDateRangeChanged)));
/// Gets or sets the command to execute when a date is picked. public static readonly DependencyProperty VisibleDateBackgroundProperty = DependencyProperty.Register(nameof(VisibleDateBackground), typeof(Brush), typeof(WinoCalendarView), new PropertyMetadata(null, new PropertyChangedCallback(OnPropertiesChanged)));
/// Unused. public static readonly DependencyProperty DateClickedCommandProperty = DependencyProperty.Register(nameof(DateClickedCommand), typeof(ICommand), typeof(WinoCalendarView), new PropertyMetadata(null));
/// </summary> public static readonly DependencyProperty TodayBackgroundColorProperty = DependencyProperty.Register(nameof(TodayBackgroundColor), typeof(Color), typeof(WinoCalendarView), new PropertyMetadata(null));
public ICommand DateClickedCommand
{
get { return (ICommand)GetValue(DateClickedCommandProperty); }
set { SetValue(DateClickedCommandProperty, value); }
}
/// <summary> public Color TodayBackgroundColor
/// 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; get { return (Color)GetValue(TodayBackgroundColorProperty); }
SetInnerDisplayDate(clickedDate); set { SetValue(TodayBackgroundColorProperty, value); }
var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
DateClickedCommand?.Execute(clickArgs);
} }
// Reset selection, we don't show selected dates but react to them. /// <summary>
CalendarView.SelectedDates.Clear(); /// Gets or sets the command to execute when a date is picked.
} /// Unused.
/// </summary>
private static void OnPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) public ICommand DateClickedCommand
{
if (d is WinoCalendarView control)
{ {
control.UpdateVisibleDateRangeBackgrounds(); get { return (ICommand)GetValue(DateClickedCommandProperty); }
set { SetValue(DateClickedCommandProperty, value); }
} }
}
private void SetInnerDisplayDate(DateTime dateTime) => CalendarView?.SetDisplayDate(dateTime); /// <summary>
/// Gets or sets the highlighted range of dates.
// Changing selected dates will trigger the selection changed event. /// </summary>
// It will behave like user clicked the date. public DateRange HighlightedDateRange
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); get { return (DateRange)GetValue(HighlightedDateRangeProperty); }
control.UpdateVisibleDateRangeBackgrounds(); set { SetValue(HighlightedDateRangeProperty, value); }
} }
}
public void UpdateVisibleDateRangeBackgrounds() public Brush VisibleDateBackground
{
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); get { return (Brush)GetValue(VisibleDateBackgroundProperty); }
set { SetValue(VisibleDateBackgroundProperty, value); }
}
if (border == null) return;
if (calendarDayItem.Date.Date == DateTime.Today.Date)
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)
{ {
border.Background = new SolidColorBrush(TodayBackgroundColor); var clickedDate = args.AddedDates[0].Date;
SetInnerDisplayDate(clickedDate);
var clickArgs = new CalendarViewDayClickedEventArgs(clickedDate);
DateClickedCommand?.Execute(clickArgs);
} }
else if (calendarDayItem.Date.Date >= HighlightedDateRange.StartDate.Date && calendarDayItem.Date.Date < HighlightedDateRange.EndDate.Date)
// 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)
{ {
border.Background = VisibleDateBackground; control.UpdateVisibleDateRangeBackgrounds();
} }
else }
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)
{ {
border.Background = null; 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;
}
} }
} }
} }

View File

@@ -10,268 +10,269 @@ using Windows.UI.Xaml.Media;
using Wino.Calendar.Args; using Wino.Calendar.Args;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Calendar.Controls; namespace Wino.Calendar.Controls
public partial class WinoDayTimelineCanvas : Control, IDisposable
{ {
public event EventHandler<TimelineCellSelectedArgs> TimelineCellSelected; public class WinoDayTimelineCanvas : Control, IDisposable
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); } public event EventHandler<TimelineCellSelectedArgs> TimelineCellSelected;
set { SetValue(PositionerUIElementProperty, value); } public event EventHandler<TimelineCellUnselectedArgs> TimelineCellUnselected;
}
public CalendarRenderOptions RenderOptions private const string PART_InternalCanvas = nameof(PART_InternalCanvas);
{ private CanvasControl Canvas;
get { return (CalendarRenderOptions)GetValue(RenderOptionsProperty); }
set { SetValue(RenderOptionsProperty, value); }
}
public SolidColorBrush HalfHourSeperatorColor 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)));
get { return (SolidColorBrush)GetValue(HalfHourSeperatorColorProperty); } public static readonly DependencyProperty HalfHourSeperatorColorProperty = DependencyProperty.Register(nameof(HalfHourSeperatorColor), typeof(SolidColorBrush), typeof(WinoDayTimelineCanvas), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged)));
set { SetValue(HalfHourSeperatorColorProperty, value); } 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 SolidColorBrush SeperatorColor public UIElement PositionerUIElement
{
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) 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)
{ {
control.RaiseCellUnselected(); if (e.OldValue != null && e.NewValue == null)
}
control.ForceDraw();
}
}
private void RaiseCellUnselected()
{
TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
}
private void OnCanvasPointerPressed(object sender, Windows.UI.Xaml.Input.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); control.RaiseCellUnselected();
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. control.ForceDraw();
double lineY = y + rectHeight / 2; }
}
args.DrawingSession.DrawLine((float)x, (float)lineY, (float)(x + rectWidth), (float)lineY, HalfHourSeperatorColor.Color, strokeThickness, new CanvasStrokeStyle() private void RaiseCellUnselected()
{ {
DashStyle = CanvasDashStyle.Dot TimelineCellUnselected?.Invoke(this, new TimelineCellUnselectedArgs());
}); }
private void OnCanvasPointerPressed(object sender, Windows.UI.Xaml.Input.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));
} }
// Draw selected item background color for the date if possible. Debug.WriteLine($"Clicked: {clickedDateTime}");
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. public WinoDayTimelineCanvas()
if (selectedDateTime.TimeOfDay.Minutes == 30) {
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)
{ {
selectedY += rectHeight / 2; var backgroundRectangle = new Rect(x + 1, y + 1, rectWidth - 1, rectHeight - 1);
args.DrawingSession.DrawRectangle(backgroundRectangle, strokeColor, strokeThickness);
args.DrawingSession.FillRectangle(backgroundRectangle, WorkingHourCellBackgroundColor.Color);
} }
var selectedRectangle = new Rect(day * rectWidth, selectedY, rectWidth, selectionRectHeight); // Draw a line in the center of the rectangle for representing half hours.
args.DrawingSession.FillRectangle(selectedRectangle, SelectedCellBackgroundBrush.Color); 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() public void Dispose()
{ {
if (Canvas == null) return; if (Canvas == null) return;
Canvas.Draw -= OnCanvasDraw; Canvas.Draw -= OnCanvasDraw;
Canvas.PointerPressed -= OnCanvasPointerPressed; Canvas.PointerPressed -= OnCanvasPointerPressed;
Canvas.RemoveFromVisualTree(); Canvas.RemoveFromVisualTree();
Canvas = null; Canvas = null;
}
} }
} }

View File

@@ -10,97 +10,98 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Helpers; using Wino.Helpers;
namespace Wino.Calendar.Helpers; namespace Wino.Calendar.Helpers
public static class CalendarXamlHelpers
{ {
public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection) public static class CalendarXamlHelpers
=> (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; public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection)
=> (CalendarItemViewModel)collection.AllDayEvents.FirstOrDefault();
var start = calendarItemViewModel.Period.Start; /// <summary>
var end = calendarItemViewModel.Period.End; /// Returns full date + duration info in Event Details page details title.
/// </summary>
string timeFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "h:mm tt" : "HH:mm"; public static string GetEventDetailsDateString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
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)}"; if (calendarItemViewModel == null || settings == null) return string.Empty;
}
else
{
return $"{start.ToString(dateFormat, settings.CultureInfo)} - {end.ToString(timeFormat, settings.CultureInfo)}";
}
}
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel) var start = calendarItemViewModel.Period.Start;
{ var end = calendarItemViewModel.Period.End;
if (calendarItemViewModel == null || !calendarItemViewModel.IsRecurringChild) return string.Empty;
// Parse recurrence rules string timeFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "h:mm tt" : "HH:mm";
var calendarEvent = new CalendarEvent string dateFormat = settings.DayHeaderDisplayType == DayHeaderDisplayType.TwelveHour ? "dddd, dd MMMM h:mm tt" : "dddd, dd MMMM HH:mm";
{
Start = new CalDateTime(calendarItemViewModel.StartDate),
End = new CalDateTime(calendarItemViewModel.EndDate),
};
var recurrenceLines = Regex.Split(calendarItemViewModel.CalendarItem.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator); if (calendarItemViewModel.IsMultiDayEvent)
{
foreach (var line in recurrenceLines) return $"{start.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)} - {end.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)}";
{ }
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line)); else
{
return $"{start.ToString(dateFormat, settings.CultureInfo)} - {end.ToString(timeFormat, settings.CultureInfo)}";
}
} }
if (calendarEvent.RecurrenceRules == null || !calendarEvent.RecurrenceRules.Any()) public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
{ {
return "No recurrence pattern."; 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}.";
} }
var recurrenceRule = calendarEvent.RecurrenceRules.First(); public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
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)}"; 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})";
}
} }
else if (calendarItemViewModel.IsMultiDayEvent)
public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
CalendarItemViewModel calendarItemViewModel,
CalendarDisplayType calendarDisplayType)
{ {
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}"; 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);
} }
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);
} }
} }

View File

@@ -15,15 +15,16 @@ using Windows.UI.Xaml.Navigation;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 // The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
namespace Wino.Calendar; namespace Wino.Calendar
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{ {
public MainPage() /// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{ {
this.InitializeComponent(); public MainPage()
{
this.InitializeComponent();
}
} }
} }

View File

@@ -1,16 +1,17 @@
namespace Wino.Calendar.Models; namespace Wino.Calendar.Models
public struct CalendarItemMeasurement
{ {
// Where to start? public struct CalendarItemMeasurement
public double Left { get; set; }
// Extend until where?
public double Right { get; set; }
public CalendarItemMeasurement(double left, double right)
{ {
Left = left; // Where to start?
Right = right; public double Left { get; set; }
// Extend until where?
public double Right { get; set; }
public CalendarItemMeasurement(double left, double right)
{
Left = left;
Right = right;
}
} }
} }

View File

@@ -8,11 +8,6 @@
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
IgnorableNamespaces="uap mp"> IgnorableNamespaces="uap mp">
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.15.0" />
<!-- Publisher Cache Folders --> <!-- Publisher Cache Folders -->
<Extensions> <Extensions>
<Extension Category="windows.publisherCacheFolders"> <Extension Category="windows.publisherCacheFolders">
@@ -21,8 +16,13 @@
</PublisherCacheFolders> </PublisherCacheFolders>
</Extension> </Extension>
</Extensions> </Extensions>
<Identity
Name="58272BurakKSE.WinoCalendar"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.0.15.0" />
<mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="f047b7dd-96ec-4d54-a862-9321e271e449" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties> <Properties>
<DisplayName>Wino Calendar</DisplayName> <DisplayName>Wino Calendar</DisplayName>

View File

@@ -0,0 +1,29 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Wino.Calendar")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wino.Calendar")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: ComVisible(false)]

View File

@@ -0,0 +1,44 @@
<!--
This file contains Runtime Directives used by .NET Native. The defaults here are suitable for most
developers. However, you can modify these parameters to modify the behavior of the .NET Native
optimizer.
Runtime Directives are documented at https://go.microsoft.com/fwlink/?LinkID=391919
To fully enable reflection for App1.MyClass and all of its public/private members
<Type Name="App1.MyClass" Dynamic="Required All"/>
To enable dynamic creation of the specific instantiation of AppClass<T> over System.Int32
<TypeInstantiation Name="App1.AppClass" Arguments="System.Int32" Activate="Required Public" />
Using the Namespace directive to apply reflection policy to all the types in a particular namespace
<Namespace Name="DataClasses.ViewModels" Serialize="All" />
-->
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Application>
<!--
An Assembly element with Name="*Application*" applies to all assemblies in
the application package. The asterisks are not wildcards.
-->
<Assembly Name="*Application*" Dynamic="Required All" />
<!-- Reduce memory footprint when building with Microsoft.Graph -->
<Assembly Name="Microsoft.Graph" Serialize="Excluded" />
<Assembly Name="Microsoft.Kiota.Abstractions" Dynamic="Public" />
<Assembly Name="Microsoft.Kiota.Authentication.Azure" Dynamic="Public" />
<Assembly Name="Microsoft.Kiota.Http.HttpClientLibrary" Dynamic="Public" />
<Assembly Name="Microsoft.Kiota.Serialization.Form" Dynamic="Public" />
<Assembly Name="Microsoft.Kiota.Serialization.Json" Dynamic="Public" />
<Assembly Name="Microsoft.Kiota.Serialization.Multipart" Dynamic="Public" />
<!-- Add your application specific runtime directives here. -->
<Type Name="Windows.Foundation.TypedEventHandler{Microsoft.UI.Xaml.Controls.NavigationView,Microsoft.UI.Xaml.Controls.NavigationViewItemInvokedEventArgs}" MarshalObject="Public" />
<Type Name="Microsoft.UI.Xaml.Controls.NavigationView">
<Event Name="ItemInvoked" Dynamic="Required"/>
</Type>
</Application>
</Directives>

View File

@@ -1,7 +0,0 @@
{
"profiles": {
"Wino.Calendar": {
"commandName": "MsixPackage"
}
}
}

View File

@@ -2,20 +2,21 @@
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
namespace Wino.Calendar.Selectors; namespace Wino.Calendar.Selectors
public partial class CustomAreaCalendarItemSelector : DataTemplateSelector
{ {
public DataTemplate AllDayTemplate { get; set; } public class CustomAreaCalendarItemSelector : DataTemplateSelector
public DataTemplate MultiDayTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{ {
if (item is CalendarItemViewModel calendarItemViewModel) public DataTemplate AllDayTemplate { get; set; }
{ public DataTemplate MultiDayTemplate { get; set; }
return calendarItemViewModel.IsMultiDayEvent ? MultiDayTemplate : AllDayTemplate;
}
return base.SelectTemplateCore(item, container); protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item is CalendarItemViewModel calendarItemViewModel)
{
return calendarItemViewModel.IsMultiDayEvent ? MultiDayTemplate : AllDayTemplate;
}
return base.SelectTemplateCore(item, container);
}
} }
} }

View File

@@ -2,32 +2,33 @@
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Calendar.Selectors; namespace Wino.Calendar.Selectors
public partial class WinoCalendarItemTemplateSelector : DataTemplateSelector
{ {
public CalendarDisplayType DisplayType { get; set; } public class WinoCalendarItemTemplateSelector : DataTemplateSelector
public DataTemplate DayWeekWorkWeekTemplate { get; set; }
public DataTemplate MonthlyTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{ {
switch (DisplayType) public CalendarDisplayType DisplayType { get; set; }
{
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); 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);
}
} }
} }

View File

@@ -7,107 +7,108 @@ using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces; using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
namespace Wino.Calendar.Services; namespace Wino.Calendar.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; /// <summary>
public event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged; /// 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.
[ObservableProperty] /// </summary>
public partial ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> GroupedAccountCalendars { get; set; } public partial class AccountCalendarStateService : ObservableObject, IAccountCalendarStateService
private ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>();
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
{ {
get public event EventHandler<GroupedAccountCalendarViewModel> CollectiveAccountGroupSelectionStateChanged;
public event EventHandler<AccountCalendarViewModel> AccountCalendarSelectionStateChanged;
[ObservableProperty]
private ReadOnlyObservableCollection<GroupedAccountCalendarViewModel> groupedAccountCalendars;
private ObservableCollection<GroupedAccountCalendarViewModel> _internalGroupedAccountCalendars = new ObservableCollection<GroupedAccountCalendarViewModel>();
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
{ {
return GroupedAccountCalendars get
.SelectMany(a => a.AccountCalendars) {
.Where(b => b.IsChecked); return GroupedAccountCalendars
.SelectMany(a => a.AccountCalendars)
.Where(b => b.IsChecked);
}
} }
}
public IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable public IEnumerable<IGrouping<MailAccount, AccountCalendarViewModel>> GroupedAccountCalendarsEnumerable
{
get
{ {
return GroupedAccountCalendars get
.Select(a => a.AccountCalendars) {
.SelectMany(b => b) return GroupedAccountCalendars
.GroupBy(c => c.Account); .Select(a => a.AccountCalendars)
.SelectMany(b => b)
.GroupBy(c => c.Account);
}
} }
}
public AccountCalendarStateService() 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); GroupedAccountCalendars = new ReadOnlyObservableCollection<GroupedAccountCalendarViewModel>(_internalGroupedAccountCalendars);
} }
}
public void AddAccountCalendar(AccountCalendarViewModel accountCalendar) private void SingleGroupCalendarCollectiveStateChanged(object sender, EventArgs e)
{ => CollectiveAccountGroupSelectionStateChanged?.Invoke(this, sender as GroupedAccountCalendarViewModel);
// Find the group that this calendar belongs to.
var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
if (group == null) private void SingleCalendarSelectionStateChanged(object sender, AccountCalendarViewModel e)
=> AccountCalendarSelectionStateChanged?.Invoke(this, e);
public void AddGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
{ {
// If the group doesn't exist, create it. groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
group = new GroupedAccountCalendarViewModel(accountCalendar.Account, new[] { accountCalendar }); groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
AddGroupedAccountCalendar(group);
_internalGroupedAccountCalendars.Add(groupedAccountCalendar);
} }
else
public void RemoveGroupedAccountCalendar(GroupedAccountCalendarViewModel groupedAccountCalendar)
{ {
group.AccountCalendars.Add(accountCalendar); groupedAccountCalendar.CalendarSelectionStateChanged -= SingleCalendarSelectionStateChanged;
groupedAccountCalendar.CollectiveSelectionStateChanged -= SingleGroupCalendarCollectiveStateChanged;
_internalGroupedAccountCalendars.Remove(groupedAccountCalendar);
} }
}
public void RemoveAccountCalendar(AccountCalendarViewModel accountCalendar) public void ClearGroupedAccountCalendar()
{
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); 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);
}
} }
} }
} }

View File

@@ -1,32 +1,33 @@
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Calendar.Services; namespace Wino.Calendar.Services
public class CalendarAuthenticatorConfig : IAuthenticatorConfig
{ {
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208"; public class CalendarAuthenticatorConfig : IAuthenticatorConfig
public string[] OutlookScope => new string[]
{ {
"Calendars.Read", public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
"Calendars.Read.Shared",
"offline_access",
"Calendars.ReadBasic",
"Calendars.ReadWrite",
"Calendars.ReadWrite.Shared",
"User.Read"
};
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; public string[] OutlookScope => new string[]
{
"Calendars.Read",
"Calendars.Read.Shared",
"offline_access",
"Calendars.ReadBasic",
"Calendars.ReadWrite",
"Calendars.ReadWrite.Shared",
"User.Read"
};
public string[] GmailScope => new string[] public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
{
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
};
public string GmailTokenStoreIdentifier => "WinoCalendarGmailTokenStore"; public string[] GmailScope => new string[]
{
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
};
public string GmailTokenStoreIdentifier => "WinoCalendarGmailTokenStore";
}
} }

View File

@@ -2,13 +2,14 @@
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.UWP.Services; using Wino.Core.UWP.Services;
namespace Wino.Calendar.Services; namespace Wino.Calendar.Services
public class DialogService : DialogServiceBase, ICalendarDialogService
{ {
public DialogService(IThemeService themeService, public class DialogService : DialogServiceBase, ICalendarDialogService
IConfigurationService configurationService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
{ {
public DialogService(IThemeService themeService,
IConfigurationService configurationService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager) : base(themeService, configurationService, applicationResourceManager)
{
}
} }
} }

View File

@@ -10,53 +10,54 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Core.UWP.Services; using Wino.Core.UWP.Services;
using Wino.Views; using Wino.Views;
namespace Wino.Calendar.Services; namespace Wino.Calendar.Services
public class NavigationService : NavigationServiceBase, INavigationService
{ {
public Type GetPageType(WinoPage winoPage) public class NavigationService : NavigationServiceBase, INavigationService
{ {
return winoPage switch public Type GetPageType(WinoPage winoPage)
{ {
WinoPage.CalendarPage => typeof(CalendarPage), return winoPage switch
WinoPage.SettingsPage => typeof(SettingsPage),
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
WinoPage.AccountManagementPage => typeof(AccountManagementPage),
WinoPage.ManageAccountsPage => typeof(ManageAccountsPage),
WinoPage.PersonalizationPage => typeof(PersonalizationPage),
WinoPage.AccountDetailsPage => typeof(AccountDetailsPage),
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
_ => throw new Exception("Page is not implemented yet."),
};
}
public void GoBack()
{
if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage)
{
var shellFrame = shellPage.GetShellFrame();
if (shellFrame.CanGoBack)
{ {
shellFrame.GoBack(); WinoPage.CalendarPage => typeof(CalendarPage),
WinoPage.SettingsPage => typeof(SettingsPage),
WinoPage.CalendarSettingsPage => typeof(CalendarSettingsPage),
WinoPage.AccountManagementPage => typeof(AccountManagementPage),
WinoPage.ManageAccountsPage => typeof(ManageAccountsPage),
WinoPage.PersonalizationPage => typeof(PersonalizationPage),
WinoPage.AccountDetailsPage => typeof(AccountDetailsPage),
WinoPage.EventDetailsPage => typeof(EventDetailsPage),
_ => throw new Exception("Page is not implemented yet."),
};
}
public void GoBack()
{
if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage)
{
var shellFrame = shellPage.GetShellFrame();
if (shellFrame.CanGoBack)
{
shellFrame.GoBack();
}
} }
} }
}
public bool Navigate(WinoPage page, object parameter = null, NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame, NavigationTransitionType transition = NavigationTransitionType.None) public bool Navigate(WinoPage page, object parameter = null, NavigationReferenceFrame frame = NavigationReferenceFrame.ShellFrame, NavigationTransitionType transition = NavigationTransitionType.None)
{
// All navigations are performed on shell frame for calendar.
if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage)
{ {
var shellFrame = shellPage.GetShellFrame(); // All navigations are performed on shell frame for calendar.
var pageType = GetPageType(page); if (Window.Current.Content is Frame appFrame && appFrame.Content is AppShell shellPage)
{
var shellFrame = shellPage.GetShellFrame();
shellFrame.Navigate(pageType, parameter); var pageType = GetPageType(page);
return true;
shellFrame.Navigate(pageType, parameter);
return true;
}
return false;
} }
return false;
} }
} }

View File

@@ -4,32 +4,33 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts; using Wino.Core.Domain.Models.Accounts;
namespace Wino.Calendar.Services; namespace Wino.Calendar.Services
public class ProviderService : IProviderService
{ {
public IProviderDetail GetProviderDetail(MailProviderType type) public class ProviderService : IProviderService
{ {
var details = GetAvailableProviders(); public IProviderDetail GetProviderDetail(MailProviderType type)
return details.FirstOrDefault(a => a.Type == type);
}
public List<IProviderDetail> GetAvailableProviders()
{
var providerList = new List<IProviderDetail>();
var providers = new MailProviderType[]
{ {
MailProviderType.Outlook, var details = GetAvailableProviders();
MailProviderType.Gmail
};
foreach (var type in providers) return details.FirstOrDefault(a => a.Type == type);
{
providerList.Add(new ProviderDetail(type, SpecialImapProvider.None));
} }
return providerList; public List<IProviderDetail> GetAvailableProviders()
{
var providerList = new List<IProviderDetail>();
var providers = new MailProviderType[]
{
MailProviderType.Outlook,
MailProviderType.Gmail
};
foreach (var type in providers)
{
providerList.Add(new ProviderDetail(type, SpecialImapProvider.None));
}
return providerList;
}
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,12 @@
using Windows.UI.Xaml; using Windows.UI.Xaml;
namespace Wino.Calendar.Styles; namespace Wino.Calendar.Styles
public sealed partial class WinoCalendarResources : ResourceDictionary
{ {
public WinoCalendarResources() public sealed partial class WinoCalendarResources : ResourceDictionary
{ {
this.InitializeComponent(); public WinoCalendarResources()
{
this.InitializeComponent();
}
} }
} }

View File

@@ -1,6 +1,7 @@
using Wino.Calendar.ViewModels; using Wino.Calendar.ViewModels;
using Wino.Core.UWP; using Wino.Core.UWP;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public abstract class AccountDetailsPageAbstract : BasePage<AccountDetailsPageViewModel> { } public abstract class AccountDetailsPageAbstract : BasePage<AccountDetailsPageViewModel> { }
}

View File

@@ -1,6 +1,7 @@
using Wino.Calendar.ViewModels; using Wino.Calendar.ViewModels;
using Wino.Core.UWP; using Wino.Core.UWP;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public partial class AccountManagementPageAbstract : BasePage<AccountManagementViewModel> { } public class AccountManagementPageAbstract : BasePage<AccountManagementViewModel> { }
}

View File

@@ -1,6 +1,7 @@
using Wino.Calendar.ViewModels; using Wino.Calendar.ViewModels;
using Wino.Core.UWP; using Wino.Core.UWP;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public abstract class AppShellAbstract : BasePage<AppShellViewModel> { } public abstract class AppShellAbstract : BasePage<AppShellViewModel> { }
}

View File

@@ -1,6 +1,7 @@
using Wino.Calendar.ViewModels; using Wino.Calendar.ViewModels;
using Wino.Core.UWP; using Wino.Core.UWP;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public abstract class CalendarPageAbstract : BasePage<CalendarPageViewModel> { } public abstract class CalendarPageAbstract : BasePage<CalendarPageViewModel> { }
}

View File

@@ -1,6 +1,7 @@
using Wino.Calendar.ViewModels; using Wino.Calendar.ViewModels;
using Wino.Core.UWP; using Wino.Core.UWP;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public abstract class CalendarSettingsPageAbstract : BasePage<CalendarSettingsPageViewModel> { } public abstract class CalendarSettingsPageAbstract : BasePage<CalendarSettingsPageViewModel> { }
}

View File

@@ -1,6 +1,7 @@
using Wino.Calendar.ViewModels; using Wino.Calendar.ViewModels;
using Wino.Core.UWP; using Wino.Core.UWP;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public abstract class EventDetailsPageAbstract : BasePage<EventDetailsPageViewModel> { } public abstract class EventDetailsPageAbstract : BasePage<EventDetailsPageViewModel> { }
}

View File

@@ -1,6 +1,7 @@
using Wino.Core.UWP; using Wino.Core.UWP;
using Wino.Core.ViewModels; using Wino.Core.ViewModels;
namespace Wino.Calendar.Views.Abstract; namespace Wino.Calendar.Views.Abstract
{
public partial class PersonalizationPageAbstract : BasePage<PersonalizationPageViewModel> { } public class PersonalizationPageAbstract : BasePage<PersonalizationPageViewModel> { }
}

View File

@@ -1,11 +1,12 @@
using Wino.Calendar.Views.Abstract; using Wino.Calendar.Views.Abstract;
namespace Wino.Calendar.Views.Account; namespace Wino.Calendar.Views.Account
public sealed partial class AccountManagementPage : AccountManagementPageAbstract
{ {
public AccountManagementPage() public sealed partial class AccountManagementPage : AccountManagementPageAbstract
{ {
InitializeComponent(); public AccountManagementPage()
{
InitializeComponent();
}
} }
} }

View File

@@ -5,49 +5,50 @@ using Wino.Calendar.Views.Abstract;
using Wino.Core.UWP; using Wino.Core.UWP;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.Views; namespace Wino.Calendar.Views
public sealed partial class AppShell : AppShellAbstract,
IRecipient<CalendarDisplayTypeChangedMessage>
{ {
private const string STATE_HorizontalCalendar = "HorizontalCalendar"; public sealed partial class AppShell : AppShellAbstract,
private const string STATE_VerticalCalendar = "VerticalCalendar"; IRecipient<CalendarDisplayTypeChangedMessage>
public Frame GetShellFrame() => ShellFrame;
public AppShell()
{ {
InitializeComponent(); private const string STATE_HorizontalCalendar = "HorizontalCalendar";
private const string STATE_VerticalCalendar = "VerticalCalendar";
Window.Current.SetTitleBar(DragArea); public Frame GetShellFrame() => ShellFrame;
ManageCalendarDisplayType();
}
private void ManageCalendarDisplayType() public AppShell()
{
// Go to different states based on the display type.
if (ViewModel.IsVerticalCalendar)
{ {
VisualStateManager.GoToState(this, STATE_VerticalCalendar, false); InitializeComponent();
Window.Current.SetTitleBar(DragArea);
ManageCalendarDisplayType();
} }
else
private void ManageCalendarDisplayType()
{ {
VisualStateManager.GoToState(this, STATE_HorizontalCalendar, false); // 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, Windows.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();
} }
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, Windows.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();
} }

View File

@@ -9,152 +9,153 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
namespace Wino.Calendar.Views; namespace Wino.Calendar.Views
public sealed partial class CalendarPage : CalendarPageAbstract,
IRecipient<ScrollToDateMessage>,
IRecipient<ScrollToHourMessage>,
IRecipient<GoNextDateRequestedMessage>,
IRecipient<GoPreviousDateRequestedMessage>
{ {
private const int PopupDialogOffset = 12; public sealed partial class CalendarPage : CalendarPageAbstract,
IRecipient<ScrollToDateMessage>,
public CalendarPage() IRecipient<ScrollToHourMessage>,
IRecipient<GoNextDateRequestedMessage>,
IRecipient<GoPreviousDateRequestedMessage>
{ {
InitializeComponent(); private const int PopupDialogOffset = 12;
NavigationCacheMode = NavigationCacheMode.Enabled;
ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged; public CalendarPage()
}
private void CalendarItemDetailContextChanged(object sender, EventArgs e)
{
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
{ {
var control = CalendarControl.GetCalendarItemControl(ViewModel.DisplayDetailsCalendarItemViewModel); InitializeComponent();
NavigationCacheMode = NavigationCacheMode.Enabled;
if (control != null) ViewModel.DetailsShowCalendarItemChanged += CalendarItemDetailContextChanged;
}
private void CalendarItemDetailContextChanged(object sender, EventArgs e)
{
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
{ {
EventDetailsPopup.PlacementTarget = control; var control = CalendarControl.GetCalendarItemControl(ViewModel.DisplayDetailsCalendarItemViewModel);
if (control != null)
{
EventDetailsPopup.PlacementTarget = control;
}
} }
} }
}
public void Receive(ScrollToHourMessage message) => CalendarControl.NavigateToHour(message.TimeSpan); public void Receive(ScrollToHourMessage message) => CalendarControl.NavigateToHour(message.TimeSpan);
public void Receive(ScrollToDateMessage message) => CalendarControl.NavigateToDay(message.Date); public void Receive(ScrollToDateMessage message) => CalendarControl.NavigateToDay(message.Date);
public void Receive(GoNextDateRequestedMessage message) => CalendarControl.GoNextRange(); public void Receive(GoNextDateRequestedMessage message) => CalendarControl.GoNextRange();
public void Receive(GoPreviousDateRequestedMessage message) => CalendarControl.GoPreviousRange(); public void Receive(GoPreviousDateRequestedMessage message) => CalendarControl.GoPreviousRange();
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.NavigationMode == NavigationMode.Back) return;
if (e.Parameter is CalendarPageNavigationArgs args)
{ {
if (args.RequestDefaultNavigation) base.OnNavigatedTo(e);
if (e.NavigationMode == NavigationMode.Back) return;
if (e.Parameter is CalendarPageNavigationArgs args)
{ {
// Go today. if (args.RequestDefaultNavigation)
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(DateTime.Now.Date, CalendarInitInitiative.App)); {
} // Go today.
else WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(DateTime.Now.Date, CalendarInitInitiative.App));
{ }
// Go specified date. else
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(args.NavigationDate, CalendarInitInitiative.User)); {
// Go specified date.
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(args.NavigationDate, CalendarInitInitiative.User));
}
} }
} }
}
private void CellSelected(object sender, TimelineCellSelectedArgs e) private void CellSelected(object sender, TimelineCellSelectedArgs e)
{ {
// Dismiss event details if exists and cancel the selection. // 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. // This is to prevent the event details from being displayed when the user clicks somewhere else.
if (EventDetailsPopup.IsOpen) 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)
{ {
CalendarControl.UnselectActiveTimelineCell();
ViewModel.DisplayDetailsCalendarItemViewModel = null; ViewModel.DisplayDetailsCalendarItemViewModel = null;
return;
} }
ViewModel.SelectedQuickEventDate = e.ClickedDate; private void CalendarScrolling(object sender, EventArgs e)
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, // In case of scrolling, we must dismiss the event details dialog.
// we must adjust the offset to make sure the tip is not hidden and has nice ViewModel.DisplayDetailsCalendarItemViewModel = null;
// 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;
} }
} }

View File

@@ -1,12 +1,13 @@
using Wino.Calendar.Views.Abstract; using Wino.Calendar.Views.Abstract;
namespace Wino.Calendar.Views; namespace Wino.Calendar.Views
public sealed partial class EventDetailsPage : EventDetailsPageAbstract
{ {
public EventDetailsPage() public sealed partial class EventDetailsPage : EventDetailsPageAbstract
{ {
this.InitializeComponent(); public EventDetailsPage()
{
this.InitializeComponent();
}
} }
} }

View File

@@ -20,7 +20,7 @@
<controls:SettingsCard.HeaderIcon> <controls:SettingsCard.HeaderIcon>
<PathIcon Data="F1 M 10 0.625 C 10 0.45573 10.061849 0.309246 10.185547 0.185547 C 10.309244 0.06185 10.455729 0 10.625 0 L 15.625 0 C 15.79427 0 15.940754 0.06185 16.064453 0.185547 C 16.18815 0.309246 16.25 0.45573 16.25 0.625 C 16.25 0.794271 16.18815 0.940756 16.064453 1.064453 C 15.940754 1.188152 15.79427 1.25 15.625 1.25 L 13.75 1.25 L 13.75 18.75 L 15.625 18.75 C 15.79427 18.75 15.940754 18.81185 16.064453 18.935547 C 16.18815 19.059244 16.25 19.205729 16.25 19.375 C 16.25 19.544271 16.18815 19.690756 16.064453 19.814453 C 15.940754 19.93815 15.79427 20 15.625 20 L 10.625 20 C 10.455729 20 10.309244 19.93815 10.185547 19.814453 C 10.061849 19.690756 10 19.544271 10 19.375 C 10 19.205729 10.061849 19.059244 10.185547 18.935547 C 10.309244 18.81185 10.455729 18.75 10.625 18.75 L 12.5 18.75 L 12.5 1.25 L 10.625 1.25 C 10.455729 1.25 10.309244 1.188152 10.185547 1.064453 C 10.061849 0.940756 10 0.794271 10 0.625 Z M 0 6.25 C 0 5.735678 0.097656 5.250651 0.292969 4.794922 C 0.488281 4.339193 0.756836 3.94043 1.098633 3.598633 C 1.44043 3.256836 1.837565 2.988281 2.290039 2.792969 C 2.742513 2.597656 3.229167 2.5 3.75 2.5 L 11.25 2.5 L 11.25 3.75 L 3.75 3.75 C 3.404948 3.75 3.081055 3.815105 2.77832 3.945312 C 2.475586 4.075521 2.210286 4.254558 1.982422 4.482422 C 1.754557 4.710287 1.575521 4.975587 1.445312 5.27832 C 1.315104 5.581056 1.25 5.904949 1.25 6.25 L 1.25 13.75 C 1.25 14.095053 1.315104 14.418945 1.445312 14.72168 C 1.575521 15.024414 1.754557 15.289714 1.982422 15.517578 C 2.210286 15.745443 2.475586 15.924479 2.77832 16.054688 C 3.081055 16.184896 3.404948 16.25 3.75 16.25 L 11.25 16.25 L 11.25 17.5 L 3.75 17.5 C 3.229167 17.5 2.742513 17.402344 2.290039 17.207031 C 1.837565 17.011719 1.44043 16.743164 1.098633 16.401367 C 0.756836 16.05957 0.488281 15.662436 0.292969 15.209961 C 0.097656 14.757487 0 14.270834 0 13.75 Z M 15 3.75 L 15 2.5 L 16.25 2.5 C 16.764322 2.5 17.249348 2.597656 17.705078 2.792969 C 18.160807 2.988281 18.55957 3.256836 18.901367 3.598633 C 19.243164 3.94043 19.511719 4.339193 19.707031 4.794922 C 19.902344 5.250651 20 5.735678 20 6.25 L 20 13.75 C 20 14.270834 19.902344 14.757487 19.707031 15.209961 C 19.511719 15.662436 19.243164 16.05957 18.901367 16.401367 C 18.55957 16.743164 18.160807 17.011719 17.705078 17.207031 C 17.249348 17.402344 16.764322 17.5 16.25 17.5 L 15 17.5 L 15 16.25 L 16.25 16.25 C 16.595051 16.25 16.918945 16.184896 17.22168 16.054688 C 17.524414 15.924479 17.789713 15.745443 18.017578 15.517578 C 18.245441 15.289714 18.424479 15.024414 18.554688 14.72168 C 18.684895 14.418945 18.75 14.095053 18.75 13.75 L 18.75 6.25 C 18.75 5.904949 18.684895 5.581056 18.554688 5.27832 C 18.424479 4.975587 18.245441 4.710287 18.017578 4.482422 C 17.789713 4.254558 17.524414 4.075521 17.22168 3.945312 C 16.918945 3.815105 16.595051 3.75 16.25 3.75 Z M 7.441406 5.361328 C 7.389323 5.250651 7.312825 5.162761 7.211914 5.097656 C 7.111002 5.032553 6.998697 5.000001 6.875 5 C 6.751302 5.000001 6.638997 5.032553 6.538086 5.097656 C 6.437174 5.162761 6.360677 5.250651 6.308594 5.361328 L 2.871094 12.861328 C 2.799479 13.017578 2.792969 13.177084 2.851562 13.339844 C 2.910156 13.502604 3.017578 13.619792 3.173828 13.691406 C 3.330078 13.763021 3.489583 13.769531 3.652344 13.710938 C 3.815104 13.652344 3.932292 13.544922 4.003906 13.388672 L 4.84375 11.5625 L 8.896484 11.5625 L 8.90625 11.5625 L 9.746094 13.388672 C 9.817708 13.544922 9.934896 13.652344 10.097656 13.710938 C 10.260416 13.769531 10.419922 13.763021 10.576172 13.691406 C 10.732422 13.619792 10.839844 13.502604 10.898438 13.339844 C 10.957031 13.177084 10.950521 13.017578 10.878906 12.861328 Z M 5.410156 10.3125 L 6.875 7.128906 L 8.339844 10.3125 Z " /> <PathIcon Data="F1 M 10 0.625 C 10 0.45573 10.061849 0.309246 10.185547 0.185547 C 10.309244 0.06185 10.455729 0 10.625 0 L 15.625 0 C 15.79427 0 15.940754 0.06185 16.064453 0.185547 C 16.18815 0.309246 16.25 0.45573 16.25 0.625 C 16.25 0.794271 16.18815 0.940756 16.064453 1.064453 C 15.940754 1.188152 15.79427 1.25 15.625 1.25 L 13.75 1.25 L 13.75 18.75 L 15.625 18.75 C 15.79427 18.75 15.940754 18.81185 16.064453 18.935547 C 16.18815 19.059244 16.25 19.205729 16.25 19.375 C 16.25 19.544271 16.18815 19.690756 16.064453 19.814453 C 15.940754 19.93815 15.79427 20 15.625 20 L 10.625 20 C 10.455729 20 10.309244 19.93815 10.185547 19.814453 C 10.061849 19.690756 10 19.544271 10 19.375 C 10 19.205729 10.061849 19.059244 10.185547 18.935547 C 10.309244 18.81185 10.455729 18.75 10.625 18.75 L 12.5 18.75 L 12.5 1.25 L 10.625 1.25 C 10.455729 1.25 10.309244 1.188152 10.185547 1.064453 C 10.061849 0.940756 10 0.794271 10 0.625 Z M 0 6.25 C 0 5.735678 0.097656 5.250651 0.292969 4.794922 C 0.488281 4.339193 0.756836 3.94043 1.098633 3.598633 C 1.44043 3.256836 1.837565 2.988281 2.290039 2.792969 C 2.742513 2.597656 3.229167 2.5 3.75 2.5 L 11.25 2.5 L 11.25 3.75 L 3.75 3.75 C 3.404948 3.75 3.081055 3.815105 2.77832 3.945312 C 2.475586 4.075521 2.210286 4.254558 1.982422 4.482422 C 1.754557 4.710287 1.575521 4.975587 1.445312 5.27832 C 1.315104 5.581056 1.25 5.904949 1.25 6.25 L 1.25 13.75 C 1.25 14.095053 1.315104 14.418945 1.445312 14.72168 C 1.575521 15.024414 1.754557 15.289714 1.982422 15.517578 C 2.210286 15.745443 2.475586 15.924479 2.77832 16.054688 C 3.081055 16.184896 3.404948 16.25 3.75 16.25 L 11.25 16.25 L 11.25 17.5 L 3.75 17.5 C 3.229167 17.5 2.742513 17.402344 2.290039 17.207031 C 1.837565 17.011719 1.44043 16.743164 1.098633 16.401367 C 0.756836 16.05957 0.488281 15.662436 0.292969 15.209961 C 0.097656 14.757487 0 14.270834 0 13.75 Z M 15 3.75 L 15 2.5 L 16.25 2.5 C 16.764322 2.5 17.249348 2.597656 17.705078 2.792969 C 18.160807 2.988281 18.55957 3.256836 18.901367 3.598633 C 19.243164 3.94043 19.511719 4.339193 19.707031 4.794922 C 19.902344 5.250651 20 5.735678 20 6.25 L 20 13.75 C 20 14.270834 19.902344 14.757487 19.707031 15.209961 C 19.511719 15.662436 19.243164 16.05957 18.901367 16.401367 C 18.55957 16.743164 18.160807 17.011719 17.705078 17.207031 C 17.249348 17.402344 16.764322 17.5 16.25 17.5 L 15 17.5 L 15 16.25 L 16.25 16.25 C 16.595051 16.25 16.918945 16.184896 17.22168 16.054688 C 17.524414 15.924479 17.789713 15.745443 18.017578 15.517578 C 18.245441 15.289714 18.424479 15.024414 18.554688 14.72168 C 18.684895 14.418945 18.75 14.095053 18.75 13.75 L 18.75 6.25 C 18.75 5.904949 18.684895 5.581056 18.554688 5.27832 C 18.424479 4.975587 18.245441 4.710287 18.017578 4.482422 C 17.789713 4.254558 17.524414 4.075521 17.22168 3.945312 C 16.918945 3.815105 16.595051 3.75 16.25 3.75 Z M 7.441406 5.361328 C 7.389323 5.250651 7.312825 5.162761 7.211914 5.097656 C 7.111002 5.032553 6.998697 5.000001 6.875 5 C 6.751302 5.000001 6.638997 5.032553 6.538086 5.097656 C 6.437174 5.162761 6.360677 5.250651 6.308594 5.361328 L 2.871094 12.861328 C 2.799479 13.017578 2.792969 13.177084 2.851562 13.339844 C 2.910156 13.502604 3.017578 13.619792 3.173828 13.691406 C 3.330078 13.763021 3.489583 13.769531 3.652344 13.710938 C 3.815104 13.652344 3.932292 13.544922 4.003906 13.388672 L 4.84375 11.5625 L 8.896484 11.5625 L 8.90625 11.5625 L 9.746094 13.388672 C 9.817708 13.544922 9.934896 13.652344 10.097656 13.710938 C 10.260416 13.769531 10.419922 13.763021 10.576172 13.691406 C 10.732422 13.619792 10.839844 13.502604 10.898438 13.339844 C 10.957031 13.177084 10.950521 13.017578 10.878906 12.861328 Z M 5.410156 10.3125 L 6.875 7.128906 L 8.339844 10.3125 Z " />
</controls:SettingsCard.HeaderIcon> </controls:SettingsCard.HeaderIcon>
<Button Command="{x:Bind ViewModel.EditAccountDetailsCommand}" Content="{x:Bind domain:Translator.FolderOperation_Rename}" /> <Button Command="{x:Bind ViewModel.RenameAccountCommand}" Content="{x:Bind domain:Translator.FolderOperation_Rename}" />
</controls:SettingsCard> </controls:SettingsCard>
<!-- TODO --> <!-- TODO -->

View File

@@ -1,11 +1,12 @@
using Wino.Calendar.Views.Abstract; using Wino.Calendar.Views.Abstract;
namespace Wino.Calendar.Views.Settings; namespace Wino.Calendar.Views.Settings
public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract
{ {
public AccountDetailsPage() public sealed partial class AccountDetailsPage : AccountDetailsPageAbstract
{ {
this.InitializeComponent(); public AccountDetailsPage()
{
this.InitializeComponent();
}
} }
} }

View File

@@ -1,12 +1,13 @@
using Wino.Calendar.Views.Abstract; using Wino.Calendar.Views.Abstract;
namespace Wino.Calendar.Views.Settings; namespace Wino.Calendar.Views.Settings
public sealed partial class CalendarSettingsPage : CalendarSettingsPageAbstract
{ {
public CalendarSettingsPage() public sealed partial class CalendarSettingsPage : CalendarSettingsPageAbstract
{ {
InitializeComponent(); public CalendarSettingsPage()
{
InitializeComponent();
}
} }
} }

View File

@@ -1,11 +1,12 @@
using Wino.Calendar.Views.Abstract; using Wino.Calendar.Views.Abstract;
namespace Wino.Calendar.Views.Settings; namespace Wino.Calendar.Views.Settings
public sealed partial class PersonalizationPage : PersonalizationPageAbstract
{ {
public PersonalizationPage() public sealed partial class PersonalizationPage : PersonalizationPageAbstract
{ {
this.InitializeComponent(); public PersonalizationPage()
{
this.InitializeComponent();
}
} }
} }

View File

@@ -1,31 +1,184 @@
<Project Sdk="Microsoft.NET.Sdk"> <?xml version="1.0" encoding="utf-8"?>
<PropertyGroup> <Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<OutputType>WinExe</OutputType> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework> <PropertyGroup>
<TargetPlatformMinVersion>10.0.18362.0</TargetPlatformMinVersion> <LangVersion>8.0</LangVersion>
<UseUwp>true</UseUwp> <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<Platforms>x86;x64;arm64</Platforms> <!-- UWP WAM Authentication on Xbox needs this. -->
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> <UseDotNetNativeSharedAssemblyFrameworkPackage>false</UseDotNetNativeSharedAssemblyFrameworkPackage>
<DefaultLanguage>en-US</DefaultLanguage> <PackageCertificateThumbprint>
<!--<PublishAot>true</PublishAot>--> </PackageCertificateThumbprint>
<PublishProfile>win-$(Platform).pubxml</PublishProfile> <PackageCertificateKeyFile>Wino.Mail_TemporaryKey.pfx</PackageCertificateKeyFile>
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling> <GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<GenerateAppInstallerFile>True</GenerateAppInstallerFile> <AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled> <AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm> <GenerateTestArtifacts>True</GenerateTestArtifacts>
</PropertyGroup> <AppxBundle>Always</AppxBundle>
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
<ItemGroup> <HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
<Compile Remove="BundleArtifacts\**" /> </PropertyGroup>
<EmbeddedResource Remove="BundleArtifacts\**" /> <PropertyGroup>
<None Remove="BundleArtifacts\**" /> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Page Remove="BundleArtifacts\**" /> <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
</ItemGroup> <ProjectGuid>{600F4979-DB7E-409D-B7DA-B60BE4C55C35}</ProjectGuid>
<ItemGroup> <OutputType>AppContainerExe</OutputType>
<PRIResource Remove="BundleArtifacts\**" /> <AppDesignerFolder>Properties</AppDesignerFolder>
</ItemGroup> <RootNamespace>Wino.Calendar</RootNamespace>
<AssemblyName>Wino.Calendar</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WindowsXamlEnableOverview>true</WindowsXamlEnableOverview>
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x86</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>ARM64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<OutputPath>bin\ARM64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>ARM64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<PlatformTarget>x64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<PlatformTarget>x64</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
<!-- .NET Native Shit -->
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
<UseDotNetNativeSharedAssemblyFrameworkPackage>false</UseDotNetNativeSharedAssemblyFrameworkPackage>
<Use64BitCompiler>true</Use64BitCompiler>
<OutOfProcPDB>true</OutOfProcPDB>
</PropertyGroup>
<ItemGroup>
<Compile Include="Activation\DefaultActivationHandler.cs" />
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
</Compile>
<Compile Include="Args\TimelineCellSelectedArgs.cs" />
<Compile Include="Args\TimelineCellUnselectedArgs.cs" />
<Compile Include="Controls\CalendarItemCommandBarFlyout.cs" />
<Compile Include="Controls\CalendarItemControl.xaml.cs">
<DependentUpon>CalendarItemControl.xaml</DependentUpon>
</Compile>
<Compile Include="Controls\CustomCalendarFlipView.cs" />
<Compile Include="Controls\DayColumnControl.cs" />
<Compile Include="Controls\DayHeaderControl.cs" />
<Compile Include="Controls\WinoCalendarControl.cs" />
<Compile Include="Controls\WinoCalendarFlipView.cs" />
<Compile Include="Controls\WinoCalendarPanel.cs" />
<Compile Include="Controls\WinoCalendarTypeSelectorControl.cs" />
<Compile Include="Controls\WinoCalendarView.cs" />
<Compile Include="Controls\WinoDayTimelineCanvas.cs" />
<Compile Include="Helpers\CalendarXamlHelpers.cs" />
<Compile Include="MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
</Compile>
<Compile Include="Models\CalendarItemMeasurement.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Selectors\CustomAreaCalendarItemSelector.cs" />
<Compile Include="Selectors\WinoCalendarItemTemplateSelector.cs" />
<Compile Include="Services\AccountCalendarStateService.cs" />
<Compile Include="Services\CalendarAuthenticatorConfig.cs" />
<Compile Include="Services\DialogService.cs" />
<Compile Include="Services\NavigationService.cs" />
<Compile Include="Services\ProviderService.cs" />
<Compile Include="Services\SettingsBuilderService.cs" />
<Compile Include="Styles\WinoCalendarResources.xaml.cs" />
<Compile Include="Views\Abstract\AccountDetailsPageAbstract.cs" />
<Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" />
<Compile Include="Views\Abstract\AppShellAbstract.cs" />
<Compile Include="Views\Abstract\CalendarPageAbstract.cs" />
<Compile Include="Views\Abstract\CalendarSettingsPageAbstract.cs" />
<Compile Include="Views\Abstract\EventDetailsPageAbstract.cs" />
<Compile Include="Views\Abstract\PersonalizationPageAbstract.cs" />
<Compile Include="Views\Account\AccountManagementPage.xaml.cs">
<DependentUpon>AccountManagementPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\AppShell.xaml.cs">
<DependentUpon>AppShell.xaml</DependentUpon>
</Compile>
<Compile Include="Views\CalendarPage.xaml.cs">
<DependentUpon>CalendarPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\EventDetailsPage.xaml.cs">
<DependentUpon>EventDetailsPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Settings\AccountDetailsPage.xaml.cs">
<DependentUpon>AccountDetailsPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Settings\CalendarSettingsPage.xaml.cs">
<DependentUpon>CalendarSettingsPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Settings\PersonalizationPage.xaml.cs">
<DependentUpon>PersonalizationPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
<SubType>Designer</SubType>
</AppxManifest>
</ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Package.StoreAssociation.xml" /> <None Include="Package.StoreAssociation.xml" />
<Content Include="Assets\LargeTile.scale-100.png" /> <Content Include="Assets\LargeTile.scale-100.png" />
@@ -73,6 +226,7 @@
<Content Include="Assets\Wide310x150Logo.scale-125.png" /> <Content Include="Assets\Wide310x150Logo.scale-125.png" />
<Content Include="Assets\Wide310x150Logo.scale-150.png" /> <Content Include="Assets\Wide310x150Logo.scale-150.png" />
<Content Include="Assets\Wide310x150Logo.scale-400.png" /> <Content Include="Assets\Wide310x150Logo.scale-400.png" />
<Content Include="Properties\Default.rd.xml" />
<Content Include="Assets\LockScreenLogo.scale-200.png" /> <Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\SplashScreen.scale-200.png" /> <Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" /> <Content Include="Assets\Square150x150Logo.scale-200.png" />
@@ -81,18 +235,120 @@
<Content Include="Assets\Wide310x150Logo.scale-200.png" /> <Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Uwp.Controls.Primitives" /> <ApplicationDefinition Include="App.xaml">
<PackageReference Include="Microsoft.Identity.Client" /> <Generator>MSBuild:Compile</Generator>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" /> <SubType>Designer</SubType>
<PackageReference Include="Win2D.uwp" /> </ApplicationDefinition>
<Page Include="Controls\CalendarItemControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="MainPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Styles\CalendarThemeResources.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Styles\DayHeaderControl.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Styles\WinoCalendarResources.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Styles\WinoCalendarTypeSelectorControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Styles\WinoCalendarView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Styles\WinoDayTimelineCanvas.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\Account\AccountManagementPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\AppShell.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\CalendarPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\EventDetailsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Settings\AccountDetailsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Settings\CalendarSettingsPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Settings\PersonalizationPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" /> <PackageReference Include="CommunityToolkit.Uwp.Controls.Primitives">
<ProjectReference Include="..\Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj" /> <Version>8.1.240916</Version>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" /> </PackageReference>
<ProjectReference Include="..\Wino.Core.UWP\Wino.Core.UWP.csproj" /> <PackageReference Include="Microsoft.Identity.Client">
<ProjectReference Include="..\Wino.Core.ViewModels\Wino.Core.ViewModels.csproj" /> <Version>4.66.2</Version>
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" /> </PackageReference>
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj" /> <PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.14</Version>
</PackageReference>
<PackageReference Include="Win2D.uwp">
<Version>1.28.1</Version>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj">
<Project>{039affa8-c1cc-4e3b-8a31-6814d7557f74}</Project>
<Name>Wino.Calendar.ViewModels</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj">
<Project>{cf3312e5-5da0-4867-9945-49ea7598af1f}</Project>
<Name>Wino.Core.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Core.UWP\Wino.Core.UWP.csproj">
<Project>{395f19ba-1e42-495c-9db5-1a6f537fccb8}</Project>
<Name>Wino.Core.UWP</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Core.ViewModels\Wino.Core.ViewModels.csproj">
<Project>{53723ae8-7e7e-4d54-adab-0a6033255cc8}</Project>
<Name>Wino.Core.ViewModels</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj">
<Project>{0c307d7e-256f-448c-8265-5622a812fbcc}</Project>
<Name>Wino.Messaging</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Services\Wino.Services.csproj">
<Project>{bba49030-7277-48cf-b2fe-3d01cb6b6c81}</Project>
<Name>Wino.Services</Name>
</ProjectReference>
</ItemGroup>
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project> </Project>

View File

@@ -1,11 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Wino.Core.Domain;
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(bool))]
public partial class BasicTypesJsonContext : JsonSerializerContext;

View File

@@ -7,149 +7,150 @@ using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Collections; namespace Wino.Core.Domain.Collections
public class CalendarEventCollection
{ {
public event EventHandler<ICalendarItem> CalendarItemAdded; public class CalendarEventCollection
public event EventHandler<ICalendarItem> CalendarItemRemoved;
public event EventHandler CalendarItemsCleared;
private ObservableRangeCollection<ICalendarItem> _internalRegularEvents = [];
private ObservableRangeCollection<ICalendarItem> _internalAllDayEvents = [];
public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; } // TODO: Rename this to include multi-day events.
public ITimePeriod Period { get; }
public CalendarSettings Settings { get; }
private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
public CalendarEventCollection(ITimePeriod period, CalendarSettings settings)
{ {
Period = period; public event EventHandler<ICalendarItem> CalendarItemAdded;
Settings = settings; public event EventHandler<ICalendarItem> CalendarItemRemoved;
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents); public event EventHandler CalendarItemsCleared;
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
}
public bool HasCalendarEvent(AccountCalendar accountCalendar) private ObservableRangeCollection<ICalendarItem> _internalRegularEvents = [];
=> _allItems.Any(x => x.AssignedCalendar.Id == accountCalendar.Id); private ObservableRangeCollection<ICalendarItem> _internalAllDayEvents = [];
public ICalendarItem GetCalendarItem(Guid calendarItemId) public ReadOnlyObservableCollection<ICalendarItem> RegularEvents { get; }
{ public ReadOnlyObservableCollection<ICalendarItem> AllDayEvents { get; } // TODO: Rename this to include multi-day events.
return _allItems.FirstOrDefault(x => x.Id == calendarItemId); public ITimePeriod Period { get; }
} public CalendarSettings Settings { get; }
public void ClearSelectionStates() private readonly List<ICalendarItem> _allItems = new List<ICalendarItem>();
{
foreach (var item in _allItems) public CalendarEventCollection(ITimePeriod period, CalendarSettings settings)
{ {
if (item is ICalendarItemViewModel calendarItemViewModel) Period = period;
Settings = settings;
RegularEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalRegularEvents);
AllDayEvents = new ReadOnlyObservableCollection<ICalendarItem>(_internalAllDayEvents);
}
public bool HasCalendarEvent(AccountCalendar accountCalendar)
=> _allItems.Any(x => x.AssignedCalendar.Id == accountCalendar.Id);
public ICalendarItem GetCalendarItem(Guid calendarItemId)
{
return _allItems.FirstOrDefault(x => x.Id == calendarItemId);
}
public void ClearSelectionStates()
{
foreach (var item in _allItems)
{ {
calendarItemViewModel.IsSelected = false; if (item is ICalendarItemViewModel calendarItemViewModel)
{
calendarItemViewModel.IsSelected = false;
}
} }
} }
}
public void FilterByCalendars(IEnumerable<Guid> visibleCalendarIds) public void FilterByCalendars(IEnumerable<Guid> visibleCalendarIds)
{
foreach (var item in _allItems)
{ {
var collections = GetProperCollectionsForCalendarItem(item); foreach (var item in _allItems)
{
var collections = GetProperCollectionsForCalendarItem(item);
foreach (var collection in collections)
{
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item))
{
RemoveCalendarItemInternal(collection, item, false);
}
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
{
AddCalendarItemInternal(collection, item, false);
}
}
}
}
private IEnumerable<ObservableRangeCollection<ICalendarItem>> GetProperCollectionsForCalendarItem(ICalendarItem calendarItem)
{
// All-day events go to all days.
// Multi-day events go to both.
// Anything else goes to regular.
if (calendarItem.IsAllDayEvent)
{
return [_internalAllDayEvents];
}
else if (calendarItem.IsMultiDayEvent)
{
return [_internalRegularEvents, _internalAllDayEvents];
}
else
{
return [_internalRegularEvents];
}
}
public void AddCalendarItem(ICalendarItem calendarItem)
{
var collections = GetProperCollectionsForCalendarItem(calendarItem);
foreach (var collection in collections) foreach (var collection in collections)
{ {
if (!visibleCalendarIds.Contains(item.AssignedCalendar.Id) && collection.Contains(item)) AddCalendarItemInternal(collection, calendarItem);
{
RemoveCalendarItemInternal(collection, item, false);
}
else if (visibleCalendarIds.Contains(item.AssignedCalendar.Id) && !collection.Contains(item))
{
AddCalendarItemInternal(collection, item, false);
}
} }
} }
}
private IEnumerable<ObservableRangeCollection<ICalendarItem>> GetProperCollectionsForCalendarItem(ICalendarItem calendarItem) public void RemoveCalendarItem(ICalendarItem calendarItem)
{
// All-day events go to all days.
// Multi-day events go to both.
// Anything else goes to regular.
if (calendarItem.IsAllDayEvent)
{ {
return [_internalAllDayEvents]; var collections = GetProperCollectionsForCalendarItem(calendarItem);
}
else if (calendarItem.IsMultiDayEvent)
{
return [_internalRegularEvents, _internalAllDayEvents];
}
else
{
return [_internalRegularEvents];
}
}
public void AddCalendarItem(ICalendarItem calendarItem) foreach (var collection in collections)
{ {
var collections = GetProperCollectionsForCalendarItem(calendarItem); RemoveCalendarItemInternal(collection, calendarItem);
}
foreach (var collection in collections)
{
AddCalendarItemInternal(collection, calendarItem);
}
}
public void RemoveCalendarItem(ICalendarItem calendarItem)
{
var collections = GetProperCollectionsForCalendarItem(calendarItem);
foreach (var collection in collections)
{
RemoveCalendarItemInternal(collection, calendarItem);
}
}
private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
{
if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
collection.Add(calendarItem);
if (create)
{
_allItems.Add(calendarItem);
} }
CalendarItemAdded?.Invoke(this, calendarItem); private void AddCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool create = true)
}
private void RemoveCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool destroy = true)
{
if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
collection.Remove(calendarItem);
if (destroy)
{ {
_allItems.Remove(calendarItem); if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
collection.Add(calendarItem);
if (create)
{
_allItems.Add(calendarItem);
}
CalendarItemAdded?.Invoke(this, calendarItem);
} }
CalendarItemRemoved?.Invoke(this, calendarItem); private void RemoveCalendarItemInternal(ObservableRangeCollection<ICalendarItem> collection, ICalendarItem calendarItem, bool destroy = true)
} {
if (calendarItem is not ICalendarItemViewModel)
throw new ArgumentException("CalendarItem must be of type ICalendarItemViewModel", nameof(calendarItem));
public void Clear() collection.Remove(calendarItem);
{
_internalAllDayEvents.Clear();
_internalRegularEvents.Clear();
_allItems.Clear();
CalendarItemsCleared?.Invoke(this, EventArgs.Empty); if (destroy)
{
_allItems.Remove(calendarItem);
}
CalendarItemRemoved?.Invoke(this, calendarItem);
}
public void Clear()
{
_internalAllDayEvents.Clear();
_internalRegularEvents.Clear();
_allItems.Clear();
CalendarItemsCleared?.Invoke(this, EventArgs.Empty);
}
} }
} }

View File

@@ -2,40 +2,41 @@
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain.Collections; namespace Wino.Core.Domain.Collections
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
{ {
/// <summary> public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
/// Gets the range of dates that are currently displayed in the collection.
/// </summary>
public DateRange DisplayRange
{ {
get /// <summary>
/// Gets the range of dates that are currently displayed in the collection.
/// </summary>
public DateRange DisplayRange
{ {
if (Count == 0) return null; get
{
if (Count == 0) return null;
var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate; var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate; var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
return new DateRange(minimumLoadedDate, maximumLoadedDate); return new DateRange(minimumLoadedDate, maximumLoadedDate);
}
} }
}
public void RemoveCalendarItem(ICalendarItem calendarItem) public void RemoveCalendarItem(ICalendarItem calendarItem)
{
foreach (var dayRange in this)
{ {
foreach (var dayRange in this)
{
}
} }
}
public void AddCalendarItem(ICalendarItem calendarItem) public void AddCalendarItem(ICalendarItem calendarItem)
{
foreach (var dayRange in this)
{ {
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start)); foreach (var dayRange in this)
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem); {
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
}
} }
} }
} }

View File

@@ -4,170 +4,171 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
namespace Wino.Core.Domain.Collections; namespace Wino.Core.Domain.Collections
/// <summary>
/// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObservableRangeCollection<T> : ObservableCollection<T>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class. /// Represents a dynamic data collection that provides notifications when items get added, removed, or when the whole list is refreshed.
/// </summary> /// </summary>
public ObservableRangeCollection() /// <typeparam name="T"></typeparam>
: base() public class ObservableRangeCollection<T> : ObservableCollection<T>
{ {
}
/// <summary> /// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection. /// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class.
/// </summary> /// </summary>
/// <param name="collection">collection: The collection from which the elements are copied.</param> public ObservableRangeCollection()
/// <exception cref="ArgumentNullException">The collection parameter cannot be null.</exception> : base()
public ObservableRangeCollection(IEnumerable<T> collection)
: base(collection)
{
}
/// <summary>
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
/// </summary>
public void AddRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Add)
{
if (notificationMode != NotifyCollectionChangedAction.Add && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Add or Reset for AddRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var startIndex = Count;
var itemsAdded = AddArrangeCore(collection);
if (!itemsAdded)
return;
if (notificationMode == NotifyCollectionChangedAction.Reset)
{ {
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
} }
var changedItems = collection is List<T> ? (List<T>)collection : new List<T>(collection); /// <summary>
/// Initializes a new instance of the System.Collections.ObjectModel.ObservableCollection(Of T) class that contains elements copied from the specified collection.
RaiseChangeNotificationEvents( /// </summary>
action: NotifyCollectionChangedAction.Add, /// <param name="collection">collection: The collection from which the elements are copied.</param>
changedItems: changedItems, /// <exception cref="ArgumentNullException">The collection parameter cannot be null.</exception>
startingIndex: startIndex); public ObservableRangeCollection(IEnumerable<T> collection)
} : base(collection)
/// <summary>
/// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). NOTE: with notificationMode = Remove, removed items starting index is not set because items are not guaranteed to be consecutive.
/// </summary>
public void RemoveRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Reset)
{
if (notificationMode != NotifyCollectionChangedAction.Remove && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Remove or Reset for RemoveRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
if (notificationMode == NotifyCollectionChangedAction.Reset)
{ {
var raiseEvents = false; }
foreach (var item in collection)
{
Items.Remove(item);
raiseEvents = true;
}
if (raiseEvents) /// <summary>
/// Adds the elements of the specified collection to the end of the ObservableCollection(Of T).
/// </summary>
public void AddRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Add)
{
if (notificationMode != NotifyCollectionChangedAction.Add && notificationMode != NotifyCollectionChangedAction.Reset)
throw new ArgumentException("Mode must be either Add or Reset for AddRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var startIndex = Count;
var itemsAdded = AddArrangeCore(collection);
if (!itemsAdded)
return;
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset); RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return; return;
}
var changedItems = new List<T>(collection);
for (var i = 0; i < changedItems.Count; i++)
{
if (!Items.Remove(changedItems[i]))
{
changedItems.RemoveAt(i); //Can't use a foreach because changedItems is intended to be (carefully) modified
i--;
} }
var changedItems = collection is List<T> ? (List<T>)collection : new List<T>(collection);
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Add,
changedItems: changedItems,
startingIndex: startIndex);
} }
if (changedItems.Count == 0) /// <summary>
return; /// Removes the first occurence of each item in the specified collection from ObservableCollection(Of T). NOTE: with notificationMode = Remove, removed items starting index is not set because items are not guaranteed to be consecutive.
/// </summary>
RaiseChangeNotificationEvents( public void RemoveRange(IEnumerable<T> collection, NotifyCollectionChangedAction notificationMode = NotifyCollectionChangedAction.Reset)
action: NotifyCollectionChangedAction.Remove,
changedItems: changedItems);
}
/// <summary>
/// Clears the current collection and replaces it with the specified item.
/// </summary>
public void Replace(T item) => ReplaceRange(new T[] { item });
/// <summary>
/// Clears the current collection and replaces it with the specified collection.
/// </summary>
public void ReplaceRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var previouslyEmpty = Items.Count == 0;
Items.Clear();
AddArrangeCore(collection);
var currentlyEmpty = Items.Count == 0;
if (previouslyEmpty && currentlyEmpty)
return;
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
}
public void InsertRange(IEnumerable<T> items)
{
CheckReentrancy();
foreach (var item in items)
Items.Insert(0, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private bool AddArrangeCore(IEnumerable<T> collection)
{
var itemAdded = false;
foreach (var item in collection)
{ {
Items.Add(item); if (notificationMode != NotifyCollectionChangedAction.Remove && notificationMode != NotifyCollectionChangedAction.Reset)
itemAdded = true; throw new ArgumentException("Mode must be either Remove or Reset for RemoveRange.", nameof(notificationMode));
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
if (notificationMode == NotifyCollectionChangedAction.Reset)
{
var raiseEvents = false;
foreach (var item in collection)
{
Items.Remove(item);
raiseEvents = true;
}
if (raiseEvents)
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
return;
}
var changedItems = new List<T>(collection);
for (var i = 0; i < changedItems.Count; i++)
{
if (!Items.Remove(changedItems[i]))
{
changedItems.RemoveAt(i); //Can't use a foreach because changedItems is intended to be (carefully) modified
i--;
}
}
if (changedItems.Count == 0)
return;
RaiseChangeNotificationEvents(
action: NotifyCollectionChangedAction.Remove,
changedItems: changedItems);
} }
return itemAdded;
}
private void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List<T> changedItems = null, int startingIndex = -1) /// <summary>
{ /// Clears the current collection and replaces it with the specified item.
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); /// </summary>
OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); public void Replace(T item) => ReplaceRange(new T[] { item });
if (changedItems is null) /// <summary>
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action)); /// Clears the current collection and replaces it with the specified collection.
else /// </summary>
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex)); public void ReplaceRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));
CheckReentrancy();
var previouslyEmpty = Items.Count == 0;
Items.Clear();
AddArrangeCore(collection);
var currentlyEmpty = Items.Count == 0;
if (previouslyEmpty && currentlyEmpty)
return;
RaiseChangeNotificationEvents(action: NotifyCollectionChangedAction.Reset);
}
public void InsertRange(IEnumerable<T> items)
{
CheckReentrancy();
foreach (var item in items)
Items.Insert(0, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private bool AddArrangeCore(IEnumerable<T> collection)
{
var itemAdded = false;
foreach (var item in collection)
{
Items.Add(item);
itemAdded = true;
}
return itemAdded;
}
private void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List<T> changedItems = null, int startingIndex = -1)
{
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
if (changedItems is null)
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action));
else
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex));
}
} }
} }

View File

@@ -1,22 +1,23 @@
namespace Wino.Core.Domain; namespace Wino.Core.Domain
public static class Constants
{ {
/// <summary> public static class Constants
/// MIME header that exists in all the drafts created from Wino. {
/// </summary> /// <summary>
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id"; /// MIME header that exists in all the drafts created from Wino.
public const string LocalDraftStartPrefix = "localDraft_"; /// </summary>
public const string WinoLocalDraftHeader = "X-Wino-Draft-Id";
public const string LocalDraftStartPrefix = "localDraft_";
public const string CalendarEventRecurrenceRuleSeperator = "___"; public const string CalendarEventRecurrenceRuleSeperator = "___";
public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey); public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey);
public const string ToastActionKey = nameof(ToastActionKey); public const string ToastActionKey = nameof(ToastActionKey);
public const string ClientLogFile = "Client_.log"; public const string ClientLogFile = "Client_.log";
public const string ServerLogFile = "Server_.log"; public const string ServerLogFile = "Server_.log";
public const string LogArchiveFileName = "WinoLogs.zip"; public const string LogArchiveFileName = "WinoLogs.zip";
public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer); public const string WinoMailIdentiifer = nameof(WinoMailIdentiifer);
public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier); public const string WinoCalendarIdentifier = nameof(WinoCalendarIdentifier);
}
} }

View File

@@ -2,23 +2,24 @@
using SQLite; using SQLite;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar; namespace Wino.Core.Domain.Entities.Calendar
public class AccountCalendar : IAccountCalendar
{ {
[PrimaryKey] public class AccountCalendar : IAccountCalendar
public Guid Id { get; set; } {
public Guid AccountId { get; set; } [PrimaryKey]
public string RemoteCalendarId { get; set; } public Guid Id { get; set; }
public string SynchronizationDeltaToken { get; set; } public Guid AccountId { get; set; }
public string Name { get; set; } public string RemoteCalendarId { get; set; }
public bool IsPrimary { get; set; } public string SynchronizationDeltaToken { get; set; }
public bool IsExtended { get; set; } = true; public string Name { get; set; }
public bool IsPrimary { get; set; }
public bool IsExtended { get; set; } = true;
/// <summary> /// <summary>
/// Unused for now. /// Unused for now.
/// </summary> /// </summary>
public string TextColorHex { get; set; } public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; } public string BackgroundColorHex { get; set; }
public string TimeZone { get; set; } public string TimeZone { get; set; }
}
} }

View File

@@ -2,18 +2,19 @@
using SQLite; using SQLite;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar; namespace Wino.Core.Domain.Entities.Calendar
// TODO: Connect to Contact store with Wino People.
public class CalendarEventAttendee
{ {
[PrimaryKey] // TODO: Connect to Contact store with Wino People.
public Guid Id { get; set; } public class CalendarEventAttendee
public Guid CalendarItemId { get; set; } {
public string Name { get; set; } [PrimaryKey]
public string Email { get; set; } public Guid Id { get; set; }
public AttendeeStatus AttendenceStatus { get; set; } public Guid CalendarItemId { get; set; }
public bool IsOrganizer { get; set; } public string Name { get; set; }
public bool IsOptionalAttendee { get; set; } public string Email { get; set; }
public string Comment { get; set; } public AttendeeStatus AttendenceStatus { get; set; }
public bool IsOrganizer { get; set; }
public bool IsOptionalAttendee { get; set; }
public string Comment { get; set; }
}
} }

View File

@@ -5,175 +5,176 @@ using SQLite;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Entities.Calendar; namespace Wino.Core.Domain.Entities.Calendar
[DebuggerDisplay("{Title} ({StartDate} - {EndDate})")]
public class CalendarItem : ICalendarItem
{ {
[PrimaryKey] [DebuggerDisplay("{Title} ({StartDate} - {EndDate})")]
public Guid Id { get; set; } public class CalendarItem : ICalendarItem
public string RemoteEventId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate
{ {
get [PrimaryKey]
public Guid Id { get; set; }
public string RemoteEventId { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Location { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate
{ {
return StartDate.AddSeconds(DurationInSeconds); get
{
return StartDate.AddSeconds(DurationInSeconds);
}
} }
}
public TimeSpan StartDateOffset { get; set; } public TimeSpan StartDateOffset { get; set; }
public TimeSpan EndDateOffset { get; set; } public TimeSpan EndDateOffset { get; set; }
private ITimePeriod _period; private ITimePeriod _period;
public ITimePeriod Period public ITimePeriod Period
{
get
{ {
_period ??= new TimeRange(StartDate, EndDate); get
{
_period ??= new TimeRange(StartDate, EndDate);
return _period; return _period;
}
} }
}
/// <summary> /// <summary>
/// Events that starts at midnight and ends at midnight are considered all-day events. /// Events that starts at midnight and ends at midnight are considered all-day events.
/// </summary> /// </summary>
public bool IsAllDayEvent public bool IsAllDayEvent
{
get
{ {
return get
StartDate.TimeOfDay == TimeSpan.Zero && {
EndDate.TimeOfDay == TimeSpan.Zero; return
StartDate.TimeOfDay == TimeSpan.Zero &&
EndDate.TimeOfDay == TimeSpan.Zero;
}
} }
}
/// <summary> /// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences. /// Events that are either an exceptional instance of a recurring event or occurrences.
/// IsOccurrence is used to display occurrence instances of parent recurring events. /// IsOccurrence is used to display occurrence instances of parent recurring events.
/// IsOccurrence == false && IsRecurringChild == true => exceptional single instance. /// IsOccurrence == false && IsRecurringChild == true => exceptional single instance.
/// </summary> /// </summary>
public bool IsRecurringChild public bool IsRecurringChild
{
get
{ {
return RecurringCalendarItemId != null; get
{
return RecurringCalendarItemId != null;
}
} }
}
/// <summary> /// <summary>
/// Events that are either an exceptional instance of a recurring event or occurrences. /// Events that are either an exceptional instance of a recurring event or occurrences.
/// </summary> /// </summary>
public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent; public bool IsRecurringEvent => IsRecurringChild || IsRecurringParent;
/// <summary> /// <summary>
/// Events that are the master event definition of recurrence events. /// Events that are the master event definition of recurrence events.
/// </summary> /// </summary>
public bool IsRecurringParent public bool IsRecurringParent
{
get
{ {
return !string.IsNullOrEmpty(Recurrence) && RecurringCalendarItemId == null; get
{
return !string.IsNullOrEmpty(Recurrence) && RecurringCalendarItemId == null;
}
} }
}
/// <summary> /// <summary>
/// Events that are not all-day events and last more than one day are considered multi-day events. /// Events that are not all-day events and last more than one day are considered multi-day events.
/// </summary> /// </summary>
public bool IsMultiDayEvent public bool IsMultiDayEvent
{
get
{ {
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent; get
{
return Period.Duration.TotalDays >= 1 && !IsAllDayEvent;
}
} }
}
public double DurationInSeconds { get; set; } public double DurationInSeconds { get; set; }
public string Recurrence { get; set; } public string Recurrence { get; set; }
public string OrganizerDisplayName { get; set; } public string OrganizerDisplayName { get; set; }
public string OrganizerEmail { get; set; } public string OrganizerEmail { get; set; }
/// <summary> /// <summary>
/// The id of the parent calendar item of the recurring event. /// The id of the parent calendar item of the recurring event.
/// Exceptional instances are stored as a separate calendar item. /// Exceptional instances are stored as a separate calendar item.
/// This makes the calendar item a child of the recurring event. /// This makes the calendar item a child of the recurring event.
/// </summary> /// </summary>
public Guid? RecurringCalendarItemId { get; set; } public Guid? RecurringCalendarItemId { get; set; }
/// <summary> /// <summary>
/// Indicates read-only events. Default is false. /// Indicates read-only events. Default is false.
/// </summary> /// </summary>
public bool IsLocked { get; set; } public bool IsLocked { get; set; }
/// <summary> /// <summary>
/// Hidden events must not be displayed to the user. /// Hidden events must not be displayed to the user.
/// This usually happens when a child instance of recurring parent is cancelled after creation. /// This usually happens when a child instance of recurring parent is cancelled after creation.
/// </summary> /// </summary>
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
// TODO // TODO
public string CustomEventColorHex { get; set; } public string CustomEventColorHex { get; set; }
public string HtmlLink { get; set; } public string HtmlLink { get; set; }
public CalendarItemStatus Status { get; set; } public CalendarItemStatus Status { get; set; }
public CalendarItemVisibility Visibility { get; set; } public CalendarItemVisibility Visibility { get; set; }
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; }
public Guid CalendarId { get; set; } public Guid CalendarId { get; set; }
[Ignore] [Ignore]
public IAccountCalendar AssignedCalendar { get; set; } public IAccountCalendar AssignedCalendar { get; set; }
/// <summary> /// <summary>
/// Whether this item does not really exist in the database or not. /// Whether this item does not really exist in the database or not.
/// These are used to display occurrence instances of parent recurring events. /// These are used to display occurrence instances of parent recurring events.
/// </summary> /// </summary>
[Ignore] [Ignore]
public bool IsOccurrence { get; set; } public bool IsOccurrence { get; set; }
/// <summary> /// <summary>
/// Id to load information related to this event. /// Id to load information related to this event.
/// Occurrences tracked by the parent recurring event if they are not exceptional instances. /// Occurrences tracked by the parent recurring event if they are not exceptional instances.
/// Recurring children here are exceptional instances. They have their own info in the database including Id. /// Recurring children here are exceptional instances. They have their own info in the database including Id.
/// </summary> /// </summary>
public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id; public Guid EventTrackingId => IsOccurrence ? RecurringCalendarItemId.Value : Id;
public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds) public CalendarItem CreateRecurrence(DateTime startDate, double durationInSeconds)
{
// Create a copy with the new start date and duration
return new CalendarItem
{ {
Id = Guid.NewGuid(), // Create a copy with the new start date and duration
Title = Title,
Description = Description, return new CalendarItem
Location = Location, {
StartDate = startDate, Id = Guid.NewGuid(),
DurationInSeconds = durationInSeconds, Title = Title,
Recurrence = Recurrence, Description = Description,
OrganizerDisplayName = OrganizerDisplayName, Location = Location,
OrganizerEmail = OrganizerEmail, StartDate = startDate,
RecurringCalendarItemId = Id, DurationInSeconds = durationInSeconds,
AssignedCalendar = AssignedCalendar, Recurrence = Recurrence,
CalendarId = CalendarId, OrganizerDisplayName = OrganizerDisplayName,
CreatedAt = CreatedAt, OrganizerEmail = OrganizerEmail,
UpdatedAt = UpdatedAt, RecurringCalendarItemId = Id,
Visibility = Visibility, AssignedCalendar = AssignedCalendar,
Status = Status, CalendarId = CalendarId,
CustomEventColorHex = CustomEventColorHex, CreatedAt = CreatedAt,
HtmlLink = HtmlLink, UpdatedAt = UpdatedAt,
StartDateOffset = StartDateOffset, Visibility = Visibility,
EndDateOffset = EndDateOffset, Status = Status,
RemoteEventId = RemoteEventId, CustomEventColorHex = CustomEventColorHex,
IsHidden = IsHidden, HtmlLink = HtmlLink,
IsLocked = IsLocked, StartDateOffset = StartDateOffset,
IsOccurrence = true EndDateOffset = EndDateOffset,
}; RemoteEventId = RemoteEventId,
IsHidden = IsHidden,
IsLocked = IsLocked,
IsOccurrence = true
};
}
} }
} }

View File

@@ -2,14 +2,15 @@
using SQLite; using SQLite;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Calendar; namespace Wino.Core.Domain.Entities.Calendar
public class Reminder
{ {
[PrimaryKey] public class Reminder
public Guid Id { get; set; } {
public Guid CalendarItemId { get; set; } [PrimaryKey]
public Guid Id { get; set; }
public Guid CalendarItemId { get; set; }
public DateTimeOffset ReminderTime { get; set; } public DateTimeOffset ReminderTime { get; set; }
public CalendarItemReminderType ReminderType { get; set; } public CalendarItemReminderType ReminderType { get; set; }
}
} }

View File

@@ -1,16 +1,17 @@
using System; using System;
using SQLite; using SQLite;
namespace Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Entities.Mail
public class AccountSignature
{ {
[PrimaryKey] public class AccountSignature
public Guid Id { get; set; } {
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string HtmlBody { get; set; } public string HtmlBody { get; set; }
public Guid MailAccountId { get; set; } public Guid MailAccountId { get; set; }
}
} }

View File

@@ -1,62 +1,63 @@
using System; using System;
using SQLite; using SQLite;
namespace Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Entities.Mail
public class RemoteAccountAlias
{ {
/// <summary> public class RemoteAccountAlias
/// Display address of the alias. {
/// </summary> /// <summary>
public string AliasAddress { get; set; } /// Display address of the alias.
/// </summary>
public string AliasAddress { get; set; }
/// <summary> /// <summary>
/// Address to be included in Reply-To header when alias is used for sending messages. /// Address to be included in Reply-To header when alias is used for sending messages.
/// </summary> /// </summary>
public string ReplyToAddress { get; set; } public string ReplyToAddress { get; set; }
/// <summary> /// <summary>
/// Whether this alias is the primary alias for the account. /// Whether this alias is the primary alias for the account.
/// </summary> /// </summary>
public bool IsPrimary { get; set; } public bool IsPrimary { get; set; }
/// <summary> /// <summary>
/// Whether the alias is verified by the server. /// Whether the alias is verified by the server.
/// Only Gmail aliases are verified for now. /// Only Gmail aliases are verified for now.
/// Non-verified alias messages might be rejected by SMTP server. /// Non-verified alias messages might be rejected by SMTP server.
/// </summary> /// </summary>
public bool IsVerified { get; set; } public bool IsVerified { get; set; }
/// <summary> /// <summary>
/// Whether this alias is the root alias for the account. /// Whether this alias is the root alias for the account.
/// Root alias means the first alias that was created for the account. /// Root alias means the first alias that was created for the account.
/// It can't be deleted or changed. /// It can't be deleted or changed.
/// </summary> /// </summary>
public bool IsRootAlias { get; set; } public bool IsRootAlias { get; set; }
/// <summary> /// <summary>
/// Optional sender name for the alias. /// Optional sender name for the alias.
/// Falls back to account's sender name if not set when preparing messages. /// Falls back to account's sender name if not set when preparing messages.
/// Used for Gmail only. /// Used for Gmail only.
/// </summary> /// </summary>
public string AliasSenderName { get; set; } public string AliasSenderName { get; set; }
} }
public class MailAccountAlias : RemoteAccountAlias public class MailAccountAlias : RemoteAccountAlias
{ {
/// <summary> /// <summary>
/// Unique Id for the alias. /// Unique Id for the alias.
/// </summary> /// </summary>
[PrimaryKey] [PrimaryKey]
public Guid Id { get; set; } public Guid Id { get; set; }
/// <summary> /// <summary>
/// Account id that this alias is attached to. /// Account id that this alias is attached to.
/// </summary> /// </summary>
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
/// <summary> /// <summary>
/// Root aliases can't be deleted. /// Root aliases can't be deleted.
/// </summary> /// </summary>
public bool CanDelete => !IsRootAlias; public bool CanDelete => !IsRootAlias;
}
} }

View File

@@ -5,152 +5,153 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Entities.Mail
/// <summary>
/// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME.
/// </summary>
public class MailCopy : IMailItem
{ {
/// <summary> /// <summary>
/// Unique Id of the mail. /// Summary of the parsed MIME messages.
/// Wino will do non-network operations on this table and others from the original MIME.
/// </summary> /// </summary>
[PrimaryKey] public class MailCopy : IMailItem
public Guid UniqueId { get; set; } {
/// <summary>
/// Unique Id of the mail.
/// </summary>
[PrimaryKey]
public Guid UniqueId { get; set; }
/// <summary> /// <summary>
/// Not unique id of the item. Some operations held on this Id, some on the UniqueId. /// Not unique id of the item. Some operations held on this Id, some on the UniqueId.
/// Same message can be in different folder. In that case UniqueId is used. /// Same message can be in different folder. In that case UniqueId is used.
/// </summary> /// </summary>
public string Id { get; set; } public string Id { get; set; }
/// <summary> /// <summary>
/// Folder that this mail belongs to. /// Folder that this mail belongs to.
/// </summary> /// </summary>
public Guid FolderId { get; set; } public Guid FolderId { get; set; }
/// <summary> /// <summary>
/// Conversation id for the mail. /// Conversation id for the mail.
/// </summary> /// </summary>
public string ThreadId { get; set; } public string ThreadId { get; set; }
/// <summary> /// <summary>
/// MIME MessageId if exists. /// MIME MessageId if exists.
/// </summary> /// </summary>
public string MessageId { get; set; } public string MessageId { get; set; }
/// <summary> /// <summary>
/// References header from MIME /// References header from MIME
/// </summary> /// </summary>
public string References { get; set; } public string References { get; set; }
/// <summary> /// <summary>
/// In-Reply-To header from MIME /// In-Reply-To header from MIME
/// </summary> /// </summary>
public string InReplyTo { get; set; } public string InReplyTo { get; set; }
/// <summary> /// <summary>
/// Name for the sender. /// Name for the sender.
/// </summary> /// </summary>
public string FromName { get; set; } public string FromName { get; set; }
/// <summary> /// <summary>
/// Address of the sender. /// Address of the sender.
/// </summary> /// </summary>
public string FromAddress { get; set; } public string FromAddress { get; set; }
/// <summary> /// <summary>
/// Subject of the mail. /// Subject of the mail.
/// </summary> /// </summary>
public string Subject { get; set; } public string Subject { get; set; }
/// <summary> /// <summary>
/// Short preview of the content. /// Short preview of the content.
/// </summary> /// </summary>
public string PreviewText { get; set; } public string PreviewText { get; set; }
/// <summary> /// <summary>
/// Date that represents this mail has been created in provider servers. /// Date that represents this mail has been created in provider servers.
/// Stored always in UTC. /// Stored always in UTC.
/// </summary> /// </summary>
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
/// <summary> /// <summary>
/// Importance of the mail. /// Importance of the mail.
/// </summary> /// </summary>
public MailImportance Importance { get; set; } public MailImportance Importance { get; set; }
/// <summary> /// <summary>
/// Read status for the mail. /// Read status for the mail.
/// </summary> /// </summary>
public bool IsRead { get; set; } public bool IsRead { get; set; }
/// <summary> /// <summary>
/// Flag status. /// Flag status.
/// Flagged for Outlook. /// Flagged for Outlook.
/// Important for Gmail. /// Important for Gmail.
/// </summary> /// </summary>
public bool IsFlagged { get; set; } public bool IsFlagged { get; set; }
/// <summary> /// <summary>
/// To support Outlook. /// To support Outlook.
/// Gmail doesn't use it. /// Gmail doesn't use it.
/// </summary> /// </summary>
public bool IsFocused { get; set; } public bool IsFocused { get; set; }
/// <summary> /// <summary>
/// Whether mail has attachments included or not. /// Whether mail has attachments included or not.
/// </summary> /// </summary>
public bool HasAttachments { get; set; } public bool HasAttachments { get; set; }
/// <summary> /// <summary>
/// Assigned draft id. /// Assigned draft id.
/// </summary> /// </summary>
public string DraftId { get; set; } public string DraftId { get; set; }
/// <summary> /// <summary>
/// Whether this mail is only created locally. /// Whether this mail is only created locally.
/// </summary> /// </summary>
[Ignore] [Ignore]
public bool IsLocalDraft => !string.IsNullOrEmpty(DraftId) && DraftId.StartsWith(Constants.LocalDraftStartPrefix); public bool IsLocalDraft => !string.IsNullOrEmpty(DraftId) && DraftId.StartsWith(Constants.LocalDraftStartPrefix);
/// <summary> /// <summary>
/// Whether this copy is draft or not. /// Whether this copy is draft or not.
/// </summary> /// </summary>
public bool IsDraft { get; set; } public bool IsDraft { get; set; }
/// <summary> /// <summary>
/// File id that this mail is assigned to. /// File id that this mail is assigned to.
/// This Id is immutable. It's used to find the file in the file system. /// This Id is immutable. It's used to find the file in the file system.
/// Even after mapping local draft to remote draft, it will not change. /// Even after mapping local draft to remote draft, it will not change.
/// </summary> /// </summary>
public Guid FileId { get; set; } public Guid FileId { get; set; }
/// <summary> /// <summary>
/// Folder that this mail is assigned to. /// Folder that this mail is assigned to.
/// Warning: This field is not populated by queries. /// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field. /// Services or View Models are responsible for populating this field.
/// </summary> /// </summary>
[Ignore] [Ignore]
public MailItemFolder AssignedFolder { get; set; } public MailItemFolder AssignedFolder { get; set; }
/// <summary> /// <summary>
/// Account that this mail is assigned to. /// Account that this mail is assigned to.
/// Warning: This field is not populated by queries. /// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field. /// Services or View Models are responsible for populating this field.
/// </summary> /// </summary>
[Ignore] [Ignore]
public MailAccount AssignedAccount { get; set; } public MailAccount AssignedAccount { get; set; }
/// <summary> /// <summary>
/// Contact information of the sender if exists. /// Contact information of the sender if exists.
/// Warning: This field is not populated by queries. /// Warning: This field is not populated by queries.
/// Services or View Models are responsible for populating this field. /// Services or View Models are responsible for populating this field.
/// </summary> /// </summary>
[Ignore] [Ignore]
public AccountContact SenderContact { get; set; } public AccountContact SenderContact { get; set; }
public IEnumerable<Guid> GetContainingIds() => [UniqueId]; public IEnumerable<Guid> GetContainingIds() => [UniqueId];
public override string ToString() => $"{Subject} <-> {Id}"; public override string ToString() => $"{Subject} <-> {Id}";
}
} }

View File

@@ -5,70 +5,71 @@ using SQLite;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Folders;
namespace Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Entities.Mail
[DebuggerDisplay("{FolderName} - {SpecialFolderType}")]
public class MailItemFolder : IMailItemFolder
{ {
[PrimaryKey] [DebuggerDisplay("{FolderName} - {SpecialFolderType}")]
public Guid Id { get; set; } public class MailItemFolder : IMailItemFolder
public string RemoteFolderId { get; set; }
public string ParentRemoteFolderId { get; set; }
public Guid MailAccountId { get; set; }
public string FolderName { get; set; }
public SpecialFolderType SpecialFolderType { get; set; }
public bool IsSystemFolder { get; set; }
public bool IsSticky { get; set; }
public bool IsSynchronizationEnabled { get; set; }
public bool IsHidden { get; set; }
public bool ShowUnreadCount { get; set; }
public DateTime? LastSynchronizedDate { get; set; }
// For IMAP
public uint UidValidity { get; set; }
public long HighestModeSeq { get; set; }
/// <summary>
/// Outlook shares delta changes per-folder. Gmail is for per-account.
/// This is only used for Outlook provider.
/// </summary>
public string DeltaToken { get; set; }
// For GMail Labels
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
[Ignore]
public List<IMailItemFolder> ChildFolders { get; set; } = [];
// Category and Move type folders are not valid move targets.
// These folders are virtual. They don't exist on the server.
public bool IsMoveTarget => !(SpecialFolderType == SpecialFolderType.More || SpecialFolderType == SpecialFolderType.Category);
public bool ContainsSpecialFolderType(SpecialFolderType type)
{ {
if (SpecialFolderType == type) [PrimaryKey]
return true; public Guid Id { get; set; }
foreach (var child in ChildFolders) public string RemoteFolderId { get; set; }
public string ParentRemoteFolderId { get; set; }
public Guid MailAccountId { get; set; }
public string FolderName { get; set; }
public SpecialFolderType SpecialFolderType { get; set; }
public bool IsSystemFolder { get; set; }
public bool IsSticky { get; set; }
public bool IsSynchronizationEnabled { get; set; }
public bool IsHidden { get; set; }
public bool ShowUnreadCount { get; set; }
public DateTime? LastSynchronizedDate { get; set; }
// For IMAP
public uint UidValidity { get; set; }
public long HighestModeSeq { get; set; }
/// <summary>
/// Outlook shares delta changes per-folder. Gmail is for per-account.
/// This is only used for Outlook provider.
/// </summary>
public string DeltaToken { get; set; }
// For GMail Labels
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
[Ignore]
public List<IMailItemFolder> ChildFolders { get; set; } = [];
// Category and Move type folders are not valid move targets.
// These folders are virtual. They don't exist on the server.
public bool IsMoveTarget => !(SpecialFolderType == SpecialFolderType.More || SpecialFolderType == SpecialFolderType.Category);
public bool ContainsSpecialFolderType(SpecialFolderType type)
{ {
if (child.SpecialFolderType == type) if (SpecialFolderType == type)
{
return true; return true;
}
else foreach (var child in ChildFolders)
{ {
return child.ContainsSpecialFolderType(type); if (child.SpecialFolderType == type)
{
return true;
}
else
{
return child.ContainsSpecialFolderType(type);
}
} }
return false;
} }
return false; public static MailItemFolder CreateMoreFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.More, FolderName = Translator.MoreFolderNameOverride };
public static MailItemFolder CreateCategoriesFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.Category, FolderName = Translator.CategoriesFolderNameOverride };
public override string ToString() => FolderName;
} }
public static MailItemFolder CreateMoreFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.More, FolderName = Translator.MoreFolderNameOverride };
public static MailItemFolder CreateCategoriesFolder() => new MailItemFolder() { IsSticky = true, SpecialFolderType = SpecialFolderType.Category, FolderName = Translator.CategoriesFolderNameOverride };
public override string ToString() => FolderName;
} }

View File

@@ -1,12 +1,13 @@
using System; using System;
using SQLite; using SQLite;
namespace Wino.Core.Domain.Entities.Mail; namespace Wino.Core.Domain.Entities.Mail
public class MergedInbox
{ {
[PrimaryKey] public class MergedInbox
public Guid Id { get; set; } {
[PrimaryKey]
public Guid Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
}
} }

View File

@@ -2,64 +2,76 @@
using System.Collections.Generic; using System.Collections.Generic;
using SQLite; using SQLite;
namespace Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Entities.Shared
/// <summary>
/// Back storage for simple name-address book.
/// These values will be inserted during MIME fetch.
/// </summary>
// TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
// Do it.
public class AccountContact : IEquatable<AccountContact>
{ {
/// <summary> /// <summary>
/// E-mail address of the contact. /// Back storage for simple name-address book.
/// These values will be inserted during MIME fetch.
/// </summary> /// </summary>
[PrimaryKey]
public string Address { get; set; }
/// <summary> // TODO: This can easily evolve to Contact store, just like People app in Windows 10/11.
/// Display name of the contact. // Do it.
/// </summary> public class AccountContact : IEquatable<AccountContact>
public string Name { get; set; }
/// <summary>
/// Base64 encoded profile image of the contact.
/// </summary>
public string Base64ContactPicture { get; set; }
/// <summary>
/// All registered accounts have their contacts registered as root.
/// Root contacts must not be overridden by any configuration.
/// They are created on account creation.
/// </summary>
public bool IsRootContact { get; set; }
public override bool Equals(object obj)
{ {
return Equals(obj as AccountContact); /// <summary>
} /// E-mail address of the contact.
/// </summary>
[PrimaryKey]
public string Address { get; set; }
public bool Equals(AccountContact other) /// <summary>
{ /// Display name of the contact.
return other is not null && /// </summary>
Address == other.Address && public string Name { get; set; }
Name == other.Name;
}
public override int GetHashCode() /// <summary>
{ /// Base64 encoded profile image of the contact.
return HashCode.Combine(Address, Name); /// </summary>
} public string Base64ContactPicture { get; set; }
public static bool operator ==(AccountContact left, AccountContact right) /// <summary>
{ /// All registered accounts have their contacts registered as root.
return EqualityComparer<AccountContact>.Default.Equals(left, right); /// Root contacts must not be overridden by any configuration.
} /// They are created on account creation.
/// </summary>
public bool IsRootContact { get; set; }
public static bool operator !=(AccountContact left, AccountContact right) /// <summary>
{ /// Short display name of the contact.
return !(left == right); /// Eather Name or Address.
/// </summary>
public string ShortDisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? $"{Address.ToLowerInvariant()};" : $"{Name};";
public string DisplayName => Address == Name || string.IsNullOrWhiteSpace(Name) ? Address.ToLowerInvariant() : $"{Name} <{Address.ToLowerInvariant()}>";
public override bool Equals(object obj)
{
return Equals(obj as AccountContact);
}
public bool Equals(AccountContact other)
{
return other is not null &&
Address == other.Address &&
Name == other.Name;
}
public override int GetHashCode()
{
int hashCode = -1717786383;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Address);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name);
return hashCode;
}
public static bool operator ==(AccountContact left, AccountContact right)
{
return EqualityComparer<AccountContact>.Default.Equals(left, right);
}
public static bool operator !=(AccountContact left, AccountContact right)
{
return !(left == right);
}
} }
} }

View File

@@ -1,74 +1,53 @@
using System; using System;
using System.Collections.Generic;
using SQLite; using SQLite;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Entities.Shared
public class CustomServerInformation
{ {
[PrimaryKey] public class CustomServerInformation
public Guid Id { get; set; }
public Guid AccountId { get; set; }
/// <summary>
/// This field is ignored. DisplayName is stored in MailAccount as SenderName from now.
/// </summary>
[Ignore]
public string DisplayName { get; set; }
public string Address { get; set; }
public string IncomingServer { get; set; }
public string IncomingServerUsername { get; set; }
public string IncomingServerPassword { get; set; }
public string IncomingServerPort { get; set; }
public CustomIncomingServerType IncomingServerType { get; set; }
public string OutgoingServer { get; set; }
public string OutgoingServerPort { get; set; }
public string OutgoingServerUsername { get; set; }
public string OutgoingServerPassword { get; set; }
/// <summary>
/// useSSL True: SslOnConnect
/// useSSL False: StartTlsWhenAvailable
/// </summary>
public ImapConnectionSecurity IncomingServerSocketOption { get; set; }
public ImapAuthenticationMethod IncomingAuthenticationMethod { get; set; }
public ImapConnectionSecurity OutgoingServerSocketOption { get; set; }
public ImapAuthenticationMethod OutgoingAuthenticationMethod { get; set; }
public string ProxyServer { get; set; }
public string ProxyServerPort { get; set; }
/// <summary>
/// Number of concurrent clients that can connect to the server.
/// Default is 5.
/// </summary>
public int MaxConcurrentClients { get; set; }
public Dictionary<string, string> GetConnectionProperties()
{ {
// Printout the public connection properties. [PrimaryKey]
public Guid Id { get; set; }
var connectionProperties = new Dictionary<string, string> public Guid AccountId { get; set; }
{
{ "IncomingServer", IncomingServer },
{ "IncomingServerPort", IncomingServerPort },
{ "IncomingServerSocketOption", IncomingServerSocketOption.ToString() },
{ "IncomingAuthenticationMethod", IncomingAuthenticationMethod.ToString() },
{ "OutgoingServer", OutgoingServer },
{ "OutgoingServerPort", OutgoingServerPort },
{ "OutgoingServerSocketOption", OutgoingServerSocketOption.ToString() },
{ "OutgoingAuthenticationMethod", OutgoingAuthenticationMethod.ToString() },
{ "ProxyServer", ProxyServer },
{ "ProxyServerPort", ProxyServerPort }
};
return connectionProperties; /// <summary>
/// This field is ignored. DisplayName is stored in MailAccount as SenderName from now.
/// </summary>
[Ignore]
public string DisplayName { get; set; }
public string Address { get; set; }
public string IncomingServer { get; set; }
public string IncomingServerUsername { get; set; }
public string IncomingServerPassword { get; set; }
public string IncomingServerPort { get; set; }
public CustomIncomingServerType IncomingServerType { get; set; }
public string OutgoingServer { get; set; }
public string OutgoingServerPort { get; set; }
public string OutgoingServerUsername { get; set; }
public string OutgoingServerPassword { get; set; }
/// <summary>
/// useSSL True: SslOnConnect
/// useSSL False: StartTlsWhenAvailable
/// </summary>
public ImapConnectionSecurity IncomingServerSocketOption { get; set; }
public ImapAuthenticationMethod IncomingAuthenticationMethod { get; set; }
public ImapConnectionSecurity OutgoingServerSocketOption { get; set; }
public ImapAuthenticationMethod OutgoingAuthenticationMethod { get; set; }
public string ProxyServer { get; set; }
public string ProxyServerPort { get; set; }
/// <summary>
/// Number of concurrent clients that can connect to the server.
/// Default is 5.
/// </summary>
public int MaxConcurrentClients { get; set; }
} }
} }

View File

@@ -3,108 +3,109 @@ using SQLite;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Entities.Shared
public class MailAccount
{ {
[PrimaryKey] public class MailAccount
public Guid Id { get; set; } {
[PrimaryKey]
public Guid Id { get; set; }
/// <summary> /// <summary>
/// Given name of the account in Wino. /// Given name of the account in Wino.
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// TODO: Display name of the authenticated user/account. /// TODO: Display name of the authenticated user/account.
/// API integrations will query this value from the API. /// API integrations will query this value from the API.
/// IMAP is populated by user on setup dialog. /// IMAP is populated by user on setup dialog.
/// </summary> /// </summary>
public string SenderName { get; set; } public string SenderName { get; set; }
/// <summary> /// <summary>
/// Account e-mail address. /// Account e-mail address.
/// </summary> /// </summary>
public string Address { get; set; } public string Address { get; set; }
/// <summary> /// <summary>
/// Provider type of the account. Outlook,Gmail etc... /// Provider type of the account. Outlook,Gmail etc...
/// </summary> /// </summary>
public MailProviderType ProviderType { get; set; } public MailProviderType ProviderType { get; set; }
/// <summary> /// <summary>
/// For tracking mail change delta. /// For tracking mail change delta.
/// Gmail : historyId /// Gmail : historyId
/// Outlook: deltaToken /// Outlook: deltaToken
/// </summary> /// </summary>
public string SynchronizationDeltaIdentifier { get; set; } public string SynchronizationDeltaIdentifier { get; set; }
/// <summary> /// <summary>
/// For tracking calendar change delta. /// For tracking calendar change delta.
/// Gmail: It's per-calendar, so unused. /// Gmail: It's per-calendar, so unused.
/// Outlook: deltaLink /// Outlook: deltaLink
/// </summary> /// </summary>
public string CalendarSynchronizationDeltaIdentifier { get; set; } public string CalendarSynchronizationDeltaIdentifier { get; set; }
/// <summary> /// <summary>
/// TODO: Gets or sets the custom account identifier color in hex. /// TODO: Gets or sets the custom account identifier color in hex.
/// </summary> /// </summary>
public string AccountColorHex { get; set; } public string AccountColorHex { get; set; }
/// <summary> /// <summary>
/// Base64 encoded profile picture of the account. /// Base64 encoded profile picture of the account.
/// </summary> /// </summary>
public string Base64ProfilePictureData { get; set; } public string Base64ProfilePictureData { get; set; }
/// <summary> /// <summary>
/// Gets or sets the listing order of the account in the accounts list. /// Gets or sets the listing order of the account in the accounts list.
/// </summary> /// </summary>
public int Order { get; set; } public int Order { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether the account has any reason for an interactive user action to fix continue operating. /// Gets or sets whether the account has any reason for an interactive user action to fix continue operating.
/// </summary> /// </summary>
public AccountAttentionReason AttentionReason { get; set; } public AccountAttentionReason AttentionReason { get; set; }
/// <summary> /// <summary>
/// Gets or sets the id of the merged inbox this account belongs to. /// Gets or sets the id of the merged inbox this account belongs to.
/// </summary> /// </summary>
public Guid? MergedInboxId { get; set; } public Guid? MergedInboxId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the additional IMAP provider assignment for the account. /// Gets or sets the additional IMAP provider assignment for the account.
/// Providers that use IMAP as a synchronizer but have special requirements. /// Providers that use IMAP as a synchronizer but have special requirements.
/// </summary> /// </summary>
public SpecialImapProvider SpecialImapProvider { get; set; } public SpecialImapProvider SpecialImapProvider { get; set; }
/// <summary> /// <summary>
/// Contains the merged inbox this account belongs to. /// Contains the merged inbox this account belongs to.
/// Ignored for all SQLite operations. /// Ignored for all SQLite operations.
/// </summary> /// </summary>
[Ignore] [Ignore]
public MergedInbox MergedInbox { get; set; } public MergedInbox MergedInbox { get; set; }
/// <summary> /// <summary>
/// Populated only when account has custom server information. /// Populated only when account has custom server information.
/// </summary> /// </summary>
[Ignore] [Ignore]
public CustomServerInformation ServerInformation { get; set; } public CustomServerInformation ServerInformation { get; set; }
/// <summary> /// <summary>
/// Account preferences. /// Account preferences.
/// </summary> /// </summary>
[Ignore] [Ignore]
public MailAccountPreferences Preferences { get; set; } public MailAccountPreferences Preferences { get; set; }
/// <summary> /// <summary>
/// Gets whether the account can perform ProfileInformation sync type. /// Gets whether the account can perform ProfileInformation sync type.
/// </summary> /// </summary>
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Gmail; public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Gmail;
/// <summary> /// <summary>
/// Gets whether the account can perform AliasInformation sync type. /// Gets whether the account can perform AliasInformation sync type.
/// </summary> /// </summary>
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail; public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
}
} }

View File

@@ -1,53 +1,54 @@
using System; using System;
using SQLite; using SQLite;
namespace Wino.Core.Domain.Entities.Shared; namespace Wino.Core.Domain.Entities.Shared
public class MailAccountPreferences
{ {
[PrimaryKey] public class MailAccountPreferences
public Guid Id { get; set; } {
[PrimaryKey]
public Guid Id { get; set; }
/// <summary> /// <summary>
/// Id of the account in MailAccount table. /// Id of the account in MailAccount table.
/// </summary> /// </summary>
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether sent draft messages should be appended to the sent folder. /// Gets or sets whether sent draft messages should be appended to the sent folder.
/// Some IMAP servers do this automatically, some don't. /// Some IMAP servers do this automatically, some don't.
/// It's disabled by default. /// It's disabled by default.
/// </summary> /// </summary>
public bool ShouldAppendMessagesToSentFolder { get; set; } public bool ShouldAppendMessagesToSentFolder { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether the notifications are enabled for the account. /// Gets or sets whether the notifications are enabled for the account.
/// </summary> /// </summary>
public bool IsNotificationsEnabled { get; set; } public bool IsNotificationsEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether the account has Focused inbox support. /// Gets or sets whether the account has Focused inbox support.
/// Null if the account provider type doesn't support Focused inbox. /// Null if the account provider type doesn't support Focused inbox.
/// </summary> /// </summary>
public bool? IsFocusedInboxEnabled { get; set; } public bool? IsFocusedInboxEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether signature should be appended automatically. /// Gets or sets whether signature should be appended automatically.
/// </summary> /// </summary>
public bool IsSignatureEnabled { get; set; } public bool IsSignatureEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether this account's unread items should be included in taskbar badge. /// Gets or sets whether this account's unread items should be included in taskbar badge.
/// </summary> /// </summary>
public bool IsTaskbarBadgeEnabled { get; set; } = true; public bool IsTaskbarBadgeEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets signature for new messages. Null if signature is not needed. /// Gets or sets signature for new messages. Null if signature is not needed.
/// </summary> /// </summary>
public Guid? SignatureIdForNewMessages { get; set; } public Guid? SignatureIdForNewMessages { get; set; }
/// <summary> /// <summary>
/// Gets or sets signature for following messages. Null if signature is not needed. /// Gets or sets signature for following messages. Null if signature is not needed.
/// </summary> /// </summary>
public Guid? SignatureIdForFollowingMessages { get; set; } public Guid? SignatureIdForFollowingMessages { get; set; }
}
} }

View File

@@ -1,14 +0,0 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Shared;
public class Thumbnail
{
[PrimaryKey]
public string Domain { get; set; }
public string Gravatar { get; set; }
public string Favicon { get; set; }
public DateTime LastUpdated { get; set; }
}

View File

@@ -1,8 +1,9 @@
namespace Wino.Core.Domain.Enums; namespace Wino.Core.Domain.Enums
public enum AccountAttentionReason
{ {
None, public enum AccountAttentionReason
InvalidCredentials, {
MissingSystemFolderConfiguration None,
InvalidCredentials,
MissingSystemFolderConfiguration
}
} }

View File

@@ -1,7 +0,0 @@
namespace Wino.Core.Domain.Enums;
public enum AccountCacheResetReason
{
AccountRemoval,
ExpiredCache
}

Some files were not shown because too many files have changed in this diff Show More