Cleaning up the solution. Separating Shared.WinRT, Services and Synchronization. Removing synchronization from app. Reducing bundle size by 45mb.

This commit is contained in:
Burak Kaan Köse
2024-07-21 05:45:02 +02:00
parent f112f369a7
commit 495885e006
523 changed files with 2254 additions and 2375 deletions

View File

@@ -0,0 +1,41 @@
using Wino.Domain.Enums;
#if NET8_0
using Microsoft.UI.Xaml;
#else
using Microsoft.UI.Xaml;
#endif
namespace Wino.Shared.WinRT.Extensions
{
public static class ElementThemeExtensions
{
public static ApplicationElementTheme ToWinoElementTheme(this ElementTheme elementTheme)
{
switch (elementTheme)
{
case ElementTheme.Light:
return ApplicationElementTheme.Light;
case ElementTheme.Dark:
return ApplicationElementTheme.Dark;
}
return ApplicationElementTheme.Default;
}
public static ElementTheme ToWindowsElementTheme(this ApplicationElementTheme elementTheme)
{
switch (elementTheme)
{
case ApplicationElementTheme.Light:
return ElementTheme.Light;
case ApplicationElementTheme.Dark:
return ElementTheme.Dark;
}
return ElementTheme.Default;
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using Windows.Storage;
using Wino.Domain.Enums;
using Wino.Domain.Models.Personalization;
using Wino.Shared.WinRT.Services;
namespace Wino.Shared.WinRT.Models.Personalization
{
/// <summary>
/// Custom themes that are generated by users.
/// </summary>
public class CustomAppTheme : AppThemeBase
{
public CustomAppTheme(CustomThemeMetadata metadata) : base(metadata.Name, metadata.Id)
{
AccentColor = metadata.AccentColorHex;
}
public override AppThemeType AppThemeType => AppThemeType.Custom;
public override string GetBackgroundPreviewImagePath()
=> $"ms-appdata:///local/{ThemeService.CustomThemeFolderName}/{Id}_preview.jpg";
public override async Task<string> GetThemeResourceDictionaryContentAsync()
{
var customAppThemeFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///AppThemes/Custom.xaml"));
return await FileIO.ReadTextAsync(customAppThemeFile);
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Threading.Tasks;
using Windows.Storage;
using Wino.Domain.Enums;
using Wino.Domain.Models.Personalization;
namespace Wino.Shared.WinRT.Models.Personalization
{
/// <summary>
/// Forest, Nighty, Clouds etc. applies to pre-defined themes in Wino.
/// </summary>
public class PreDefinedAppTheme : AppThemeBase
{
public PreDefinedAppTheme(string themeName,
Guid id,
string accentColor = "",
ApplicationElementTheme forcedElementTheme = ApplicationElementTheme.Default) : base(themeName, id)
{
AccentColor = accentColor;
ForceElementTheme = forcedElementTheme;
}
public override AppThemeType AppThemeType => AppThemeType.PreDefined;
public override string GetBackgroundPreviewImagePath()
=> $"ms-appx:///BackgroundImages/{ThemeName}.jpg";
public override async Task<string> GetThemeResourceDictionaryContentAsync()
{
var xamlDictionaryFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///AppThemes/{ThemeName}.xaml"));
return await FileIO.ReadTextAsync(xamlDictionaryFile);
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using Wino.Domain.Enums;
namespace Wino.Shared.WinRT.Models.Personalization
{
// Mica - Acrylic.
public class SystemAppTheme : PreDefinedAppTheme
{
public SystemAppTheme(string themeName, Guid id) : base(themeName, id, "") { }
public override AppThemeType AppThemeType => AppThemeType.System;
}
}

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.Core.UWP")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Wino.Core.UWP")]
[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,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains Runtime Directives, specifications about types your application accesses
through reflection and other dynamic code patterns. Runtime Directives are used to control the
.NET Native optimizer and ensure that it does not remove code accessed by your library. If your
library does not do any reflection, then you generally do not need to edit this file. However,
if your library reflects over types, especially types passed to it or derived from its types,
then you should write Runtime Directives.
The most common use of reflection in libraries is to discover information about types passed
to the library. Runtime Directives have three ways to express requirements on types passed to
your library.
1. Parameter, GenericParameter, TypeParameter, TypeEnumerableParameter
Use these directives to reflect over types passed as a parameter.
2. SubTypes
Use a SubTypes directive to reflect over types derived from another type.
3. AttributeImplies
Use an AttributeImplies directive to indicate that your library needs to reflect over
types or methods decorated with an attribute.
For more information on writing Runtime Directives for libraries, please visit
https://go.microsoft.com/fwlink/?LinkID=391919
-->
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
<Library Name="Wino.Core.UWP">
<!-- add directives for your library here -->
</Library>
</Directives>

View File

@@ -0,0 +1,18 @@
#if NET8_0
using Microsoft.UI.Xaml;
#else
using Microsoft.UI.Xaml;
#endif
namespace Wino.Shared.WinRT.Services
{
public interface IAppShellService
{
Window AppWindow { get; set; }
}
public class AppShellService : IAppShellService
{
public Window AppWindow { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
using Wino.Domain.Interfaces;
namespace Wino.Shared.WinRT.Services
{
public class ClipboardService : IClipboardService
{
public Task CopyClipboardAsync(string text)
{
var package = new DataPackage();
package.SetText(text);
Clipboard.SetContent(package);
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.ComponentModel;
using Windows.Foundation.Collections;
using Windows.Storage;
using Wino.Domain.Interfaces;
namespace Wino.Shared.WinRT.Services
{
public class ConfigurationService : IConfigurationService
{
public T Get<T>(string key, T defaultValue = default)
=> GetInternal(key, ApplicationData.Current.LocalSettings.Values, defaultValue);
public T GetRoaming<T>(string key, T defaultValue = default)
=> GetInternal(key, ApplicationData.Current.RoamingSettings.Values, defaultValue);
public void Set(string key, object value)
=> SetInternal(key, value, ApplicationData.Current.LocalSettings.Values);
public void SetRoaming(string key, object value)
=> SetInternal(key, value, ApplicationData.Current.RoamingSettings.Values);
private T GetInternal<T>(string key, IPropertySet collection, T defaultValue = default)
{
if (collection.ContainsKey(key))
{
var value = collection[key]?.ToString();
if (typeof(T).IsEnum)
return (T)Enum.Parse(typeof(T), value);
if (typeof(T) == typeof(Guid?) && Guid.TryParse(value, out Guid guidResult))
{
return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(value);
}
return (T)Convert.ChangeType(value, typeof(T));
}
return defaultValue;
}
private void SetInternal(string key, object value, IPropertySet collection)
=> collection[key] = value?.ToString();
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using Wino.Domain.Interfaces;
namespace Wino.Shared.WinRT.Services
{
public class FileService : IFileService
{
public async Task<string> CopyFileAsync(string sourceFilePath, string destinationFolderPath)
{
var fileName = Path.GetFileName(sourceFilePath);
var sourceFileHandle = await StorageFile.GetFileFromPathAsync(sourceFilePath);
var destinationFolder = await StorageFolder.GetFolderFromPathAsync(destinationFolderPath);
var copiedFile = await sourceFileHandle.CopyAsync(destinationFolder, fileName, NameCollisionOption.GenerateUniqueName);
return copiedFile.Path;
}
public async Task<string> GetFileContentByApplicationUriAsync(string resourcePath)
{
var releaseNoteFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri(resourcePath));
return await FileIO.ReadTextAsync(releaseNoteFile);
}
public async Task<Stream> GetFileStreamAsync(string folderPath, string fileName)
{
var folder = await StorageFolder.GetFolderFromPathAsync(folderPath);
var createdFile = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
return await createdFile.OpenStreamForWriteAsync();
}
}
}

View File

@@ -0,0 +1,25 @@
using Windows.System;
using Windows.UI.Core;
using Wino.Domain.Interfaces;
#if NET8_0
using Microsoft.UI.Xaml;
#else
using Microsoft.UI.Xaml;
#endif
namespace Wino.Shared.WinRT.Services
{
public class KeyPressService : IKeyPressService
{
public bool IsCtrlKeyPressed()
=> Window.Current?.CoreWindow?.GetKeyState(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down) ?? false;
public bool IsShiftKeyPressed()
=> Window.Current?.CoreWindow?.GetKeyState(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down) ?? false;
}
}

View File

@@ -0,0 +1,157 @@
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Foundation.Metadata;
using Windows.Security.Cryptography;
using Windows.Security.Cryptography.Core;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.System;
using Windows.UI.Shell;
using Wino.Domain.Models.Authorization;
using Wino.Domain.Interfaces;
#if NET8_0
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
#else
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Security.Authentication.Web;
#endif
namespace Wino.Shared.WinRT.Services
{
public class NativeAppService : INativeAppService
{
private string _mimeMessagesFolder;
private string _editorBundlePath;
public string GetWebAuthenticationBrokerUri()
{
#if NET8_0
// WinUI
return "wino://winomail.app";
#else
return WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
#endif
}
public async Task<string> GetMimeMessageStoragePath()
{
if (!string.IsNullOrEmpty(_mimeMessagesFolder))
return _mimeMessagesFolder;
var localFolder = ApplicationData.Current.LocalFolder;
var mimeFolder = await localFolder.CreateFolderAsync("Mime", CreationCollisionOption.OpenIfExists);
_mimeMessagesFolder = mimeFolder.Path;
return _mimeMessagesFolder;
}
#region Cryptography
public string randomDataBase64url(uint length)
{
IBuffer buffer = CryptographicBuffer.GenerateRandom(length);
return base64urlencodeNoPadding(buffer);
}
public IBuffer sha256(string inputString)
{
HashAlgorithmProvider sha = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Sha256);
IBuffer buff = CryptographicBuffer.ConvertStringToBinary(inputString, BinaryStringEncoding.Utf8);
return sha.HashData(buff);
}
public string base64urlencodeNoPadding(IBuffer buffer)
{
string base64 = CryptographicBuffer.EncodeToBase64String(buffer);
// Converts base64 to base64url.
base64 = base64.Replace("+", "-");
base64 = base64.Replace("/", "_");
// Strips padding.
base64 = base64.Replace("=", "");
return base64;
}
#endregion
// GMail Integration.
public GoogleAuthorizationRequest GetGoogleAuthorizationRequest()
{
string state = randomDataBase64url(32);
string code_verifier = randomDataBase64url(32);
string code_challenge = base64urlencodeNoPadding(sha256(code_verifier));
return new GoogleAuthorizationRequest(state, code_verifier, code_challenge);
}
public async Task<string> GetEditorBundlePathAsync()
{
if (string.IsNullOrEmpty(_editorBundlePath))
{
var editorFileFromBundle = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///JS/editor.html"))
.AsTask()
.ConfigureAwait(false);
_editorBundlePath = editorFileFromBundle.Path;
}
return _editorBundlePath;
}
public bool IsAppRunning()
{
#if WINDOWS_UWP
return (Window.Current?.Content as Frame)?.Content != null;
#endif
return true;
}
public async Task LaunchFileAsync(string filePath)
{
var file = await StorageFile.GetFileFromPathAsync(filePath);
await Launcher.LaunchFileAsync(file);
}
public Task LaunchUriAsync(Uri uri) => Launcher.LaunchUriAsync(uri).AsTask();
public string GetFullAppVersion()
{
Package package = Package.Current;
PackageId packageId = package.Id;
PackageVersion version = packageId.Version;
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
}
public async Task PinAppToTaskbarAsync()
{
// If Start screen manager API's aren't present
if (!ApiInformation.IsTypePresent("Windows.UI.Shell.TaskbarManager")) return;
// Get the taskbar manager
var taskbarManager = TaskbarManager.GetDefault();
// If Taskbar doesn't allow pinning, don't show the tip
if (!taskbarManager.IsPinningAllowed) return;
// If already pinned, don't show the tip
if (await taskbarManager.IsCurrentAppPinnedAsync()) return;
await taskbarManager.RequestPinCurrentAppAsync();
}
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
using Wino.Domain;
using Wino.Domain.Entities;
using Wino.Domain;
using Wino.Domain.Enums;
using Wino.Domain.Models.MailItem;
using Wino.Domain.Interfaces;
#if NET8_0
using CommunityToolkit.WinUI.Notifications;
#else
using Microsoft.Toolkit.Uwp.Notifications;
#endif
namespace Wino.Shared.WinRT.Services
{
// TODO: Refactor this thing. It's garbage.
public class NotificationBuilder : INotificationBuilder
{
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IAccountService _accountService;
private readonly IFolderService _folderService;
private readonly IMailService _mailService;
public NotificationBuilder(IUnderlyingThemeService underlyingThemeService,
IAccountService accountService,
IFolderService folderService,
IMailService mailService)
{
_underlyingThemeService = underlyingThemeService;
_accountService = accountService;
_folderService = folderService;
_mailService = mailService;
}
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> downloadedMailItems)
{
var mailCount = downloadedMailItems.Count();
// If there are more than 3 mails, just display 1 general toast.
if (mailCount > 3)
{
var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default);
builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
builder.AddButton(GetDismissButton());
builder.Show();
}
else
{
var validItems = new List<IMailItem>();
// Fetch mails again to fill up assigned folder data and latest statuses.
// They've been marked as read by executing synchronizer tasks until inital sync finishes.
foreach (var item in downloadedMailItems)
{
var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId);
if (mailItem != null && mailItem.AssignedFolder != null)
{
validItems.Add(mailItem);
}
}
foreach (var mailItem in validItems)
{
if (mailItem.IsRead)
continue;
var builder = new ToastContentBuilder();
builder.SetToastScenario(ToastScenario.Default);
var host = ThumbnailService.GetHost(mailItem.FromAddress);
var knownTuple = ThumbnailService.CheckIsKnown(host);
bool isKnown = knownTuple.Item1;
host = knownTuple.Item2;
if (isKnown)
builder.AddAppLogoOverride(new Uri(ThumbnailService.GetKnownHostImage(host)), hintCrop: ToastGenericAppLogoCrop.Default);
else
{
// TODO: https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=toolkit
// Follow official guides for icons/theme.
bool isOSDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark();
string profileLogoName = isOSDarkTheme ? "profile-dark.png" : "profile-light.png";
builder.AddAppLogoOverride(new Uri($"ms-appx:///Assets/NotificationIcons/{profileLogoName}"), hintCrop: ToastGenericAppLogoCrop.Circle);
}
// Override system notification timetamp with received date of the mail.
// It may create confusion for some users, but still it's the truth...
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
builder.AddText(mailItem.FromName);
builder.AddText(mailItem.Subject);
builder.AddText(mailItem.PreviewText);
builder.AddArgument(Constants.ToastMailItemIdKey, mailItem.UniqueId.ToString());
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
builder.AddButton(GetMarkedAsRead(mailItem.Id, mailItem.AssignedFolder.RemoteFolderId));
builder.AddButton(GetDeleteButton(mailItem.Id, mailItem.AssignedFolder.RemoteFolderId));
builder.AddButton(GetDismissButton());
builder.Show();
}
await UpdateTaskbarIconBadgeAsync();
}
}
private ToastButton GetDismissButton()
=> new ToastButton()
.SetDismissActivation()
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png"));
private ToastButton GetDeleteButton(string mailCopyId, string remoteFolderId)
=> new ToastButton()
.SetContent(Translator.MailOperation_Delete)
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png"))
.AddArgument(Constants.ToastMailItemIdKey, mailCopyId)
.AddArgument(Constants.ToastMailItemRemoteFolderIdKey, remoteFolderId)
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete)
.SetBackgroundActivation();
private ToastButton GetMarkedAsRead(string mailCopyId, string remoteFolderId)
=> new ToastButton()
.SetContent(Translator.MailOperation_MarkAsRead)
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/markread.png"))
.AddArgument(Constants.ToastMailItemIdKey, mailCopyId)
.AddArgument(Constants.ToastMailItemRemoteFolderIdKey, remoteFolderId)
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead)
.SetBackgroundActivation();
public async Task UpdateTaskbarIconBadgeAsync()
{
int totalUnreadCount = 0;
var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication();
try
{
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
var accountInbox = await _folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Inbox);
if (accountInbox == null)
continue;
var inboxUnreadCount = await _folderService.GetFolderNotificationBadgeAsync(accountInbox.Id);
totalUnreadCount += inboxUnreadCount;
}
if (totalUnreadCount > 0)
{
// Get the blank badge XML payload for a badge number
XmlDocument badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber);
// Set the value of the badge in the XML to our number
XmlElement badgeElement = badgeXml.SelectSingleNode("/badge") as XmlElement;
badgeElement.SetAttribute("value", totalUnreadCount.ToString());
// Create the badge notification
BadgeNotification badge = new BadgeNotification(badgeXml);
// And update the badge
badgeUpdater.Update(badge);
}
else
badgeUpdater.Clear();
}
catch (Exception)
{
// TODO: Log exceptions.
badgeUpdater.Clear();
}
}
public async Task CreateTestNotificationAsync(string title, string message)
{
// with args test.
await CreateNotificationsAsync(Guid.Parse("28c3c39b-7147-4de3-b209-949bd19eede6"), new List<IMailItem>()
{
new MailCopy()
{
Subject = "test subject",
PreviewText = "preview html",
CreationDate = DateTime.UtcNow,
FromAddress = "bkaankose@outlook.com",
Id = "AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AnMdP0zg8wkS_Ib2Eeh80LAAGq91I3QAA",
}
});
//var builder = new ToastContentBuilder();
//builder.SetToastScenario(ToastScenario.Default);
//builder.AddText(title);
//builder.AddText(message);
//builder.Show();
//await Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,193 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Domain;
using Wino.Domain.Enums;
using Wino.Domain.Interfaces;
using Wino.Domain.Models.Reader;
namespace Wino.Shared.WinRT.Services
{
public class PreferencesService : ObservableObject, IPreferencesService
{
private readonly IConfigurationService _configurationService;
public event EventHandler<string> PreferenceChanged;
public PreferencesService(IConfigurationService configurationService)
{
_configurationService = configurationService;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
PreferenceChanged?.Invoke(this, e.PropertyName);
}
private void SaveProperty(string propertyName, object value) => _configurationService.Set(propertyName, value);
private void SetPropertyAndSave(string propertyName, object value)
{
_configurationService.Set(propertyName, value);
OnPropertyChanged(propertyName);
Debug.WriteLine($"PreferencesService -> {propertyName}:{value?.ToString()}");
}
public MailRenderingOptions GetRenderingOptions()
=> new MailRenderingOptions() { LoadImages = RenderImages, LoadStyles = RenderStyles };
public MailListDisplayMode MailItemDisplayMode
{
get => _configurationService.Get(nameof(MailItemDisplayMode), MailListDisplayMode.Spacious);
set => SetPropertyAndSave(nameof(MailItemDisplayMode), value);
}
public bool IsSemanticZoomEnabled
{
get => _configurationService.Get(nameof(IsSemanticZoomEnabled), true);
set => SetPropertyAndSave(nameof(IsSemanticZoomEnabled), value);
}
public bool IsHardDeleteProtectionEnabled
{
get => _configurationService.Get(nameof(IsHardDeleteProtectionEnabled), true);
set => SetPropertyAndSave(nameof(IsHardDeleteProtectionEnabled), value);
}
public bool IsThreadingEnabled
{
get => _configurationService.Get(nameof(IsThreadingEnabled), true);
set => SetPropertyAndSave(nameof(IsThreadingEnabled), value);
}
public bool IsShowSenderPicturesEnabled
{
get => _configurationService.Get(nameof(IsShowSenderPicturesEnabled), true);
set => SetPropertyAndSave(nameof(IsShowSenderPicturesEnabled), value);
}
public bool IsShowPreviewEnabled
{
get => _configurationService.Get(nameof(IsShowPreviewEnabled), true);
set => SetPropertyAndSave(nameof(IsShowPreviewEnabled), value);
}
public bool RenderStyles
{
get => _configurationService.Get(nameof(RenderStyles), true);
set => SetPropertyAndSave(nameof(RenderStyles), value);
}
public bool RenderImages
{
get => _configurationService.Get(nameof(RenderImages), true);
set => SetPropertyAndSave(nameof(RenderImages), value);
}
public bool Prefer24HourTimeFormat
{
get => _configurationService.Get(nameof(Prefer24HourTimeFormat), false);
set => SetPropertyAndSave(nameof(Prefer24HourTimeFormat), value);
}
public MailMarkAsOption MarkAsPreference
{
get => _configurationService.Get(nameof(MarkAsPreference), MailMarkAsOption.WhenSelected);
set => SetPropertyAndSave(nameof(MarkAsPreference), value);
}
public int MarkAsDelay
{
get => _configurationService.Get(nameof(MarkAsDelay), 5);
set => SetPropertyAndSave(nameof(MarkAsDelay), value);
}
public MailOperation RightSwipeOperation
{
get => _configurationService.Get(nameof(RightSwipeOperation), MailOperation.MarkAsRead);
set => SetPropertyAndSave(nameof(RightSwipeOperation), value);
}
public MailOperation LeftSwipeOperation
{
get => _configurationService.Get(nameof(LeftSwipeOperation), MailOperation.SoftDelete);
set => SetPropertyAndSave(nameof(LeftSwipeOperation), value);
}
public bool IsHoverActionsEnabled
{
get => _configurationService.Get(nameof(IsHoverActionsEnabled), true);
set => SetPropertyAndSave(nameof(IsHoverActionsEnabled), value);
}
public MailOperation LeftHoverAction
{
get => _configurationService.Get(nameof(LeftHoverAction), MailOperation.Archive);
set => SetPropertyAndSave(nameof(LeftHoverAction), value);
}
public MailOperation CenterHoverAction
{
get => _configurationService.Get(nameof(CenterHoverAction), MailOperation.SoftDelete);
set => SetPropertyAndSave(nameof(CenterHoverAction), value);
}
public MailOperation RightHoverAction
{
get => _configurationService.Get(nameof(RightHoverAction), MailOperation.SetFlag);
set => SetPropertyAndSave(nameof(RightHoverAction), value);
}
public bool IsLoggingEnabled
{
get => _configurationService.Get(nameof(IsLoggingEnabled), true);
set => SetPropertyAndSave(nameof(IsLoggingEnabled), value);
}
public bool IsMailkitProtocolLoggerEnabled
{
get => _configurationService.Get(nameof(IsMailkitProtocolLoggerEnabled), false);
set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value);
}
public Guid? StartupEntityId
{
get => _configurationService.Get<Guid?>(nameof(StartupEntityId), null);
set => SaveProperty(propertyName: nameof(StartupEntityId), value);
}
public AppLanguage CurrentLanguage
{
get => _configurationService.Get(nameof(CurrentLanguage), Constants.DefaultAppLanguage);
set => SaveProperty(propertyName: nameof(CurrentLanguage), value);
}
public ReaderFont ReaderFont
{
get => _configurationService.Get(nameof(ReaderFont), ReaderFont.Calibri);
set => SaveProperty(propertyName: nameof(ReaderFont), value);
}
public int ReaderFontSize
{
get => _configurationService.Get(nameof(ReaderFontSize), 14);
set => SaveProperty(propertyName: nameof(ReaderFontSize), value);
}
public bool IsNavigationPaneOpened
{
get => _configurationService.Get(nameof(IsNavigationPaneOpened), true);
set => SaveProperty(propertyName: nameof(IsNavigationPaneOpened), value);
}
public bool AutoSelectNextItem
{
get => _configurationService.Get(nameof(AutoSelectNextItem), true);
set => SaveProperty(propertyName: nameof(AutoSelectNextItem), value);
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Domain.Interfaces;
using Wino.Messaging.Client.Shell;
namespace Wino.Shared.WinRT.Services
{
public class StatePersistenceService : ObservableObject, IStatePersistanceService
{
public event EventHandler<string> StatePropertyChanged;
private const string OpenPaneLengthKey = nameof(OpenPaneLengthKey);
private const string MailListPaneLengthKey = nameof(MailListPaneLengthKey);
private readonly IConfigurationService _configurationService;
public StatePersistenceService(IConfigurationService configurationService)
{
_configurationService = configurationService;
openPaneLength = _configurationService.Get(OpenPaneLengthKey, 320d);
_mailListPaneLength = _configurationService.Get(MailListPaneLengthKey, 420d);
PropertyChanged += ServicePropertyChanged;
}
private void ServicePropertyChanged(object sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e.PropertyName);
public bool IsBackButtonVisible => IsReadingMail && IsReaderNarrowed;
private bool isReadingMail;
public bool IsReadingMail
{
get => isReadingMail;
set
{
if (SetProperty(ref isReadingMail, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
}
}
}
private bool shouldShiftMailRenderingDesign;
public bool ShouldShiftMailRenderingDesign
{
get { return shouldShiftMailRenderingDesign; }
set { shouldShiftMailRenderingDesign = value; }
}
private bool isReaderNarrowed;
public bool IsReaderNarrowed
{
get => isReaderNarrowed;
set
{
if (SetProperty(ref isReaderNarrowed, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
}
}
}
private string coreWindowTitle;
public string CoreWindowTitle
{
get => coreWindowTitle;
set
{
if (SetProperty(ref coreWindowTitle, value))
{
UpdateAppCoreWindowTitle();
}
}
}
#region Settings
private double openPaneLength;
public double OpenPaneLength
{
get => openPaneLength;
set
{
if (SetProperty(ref openPaneLength, value))
{
_configurationService.Set(OpenPaneLengthKey, value);
}
}
}
private double _mailListPaneLength;
public double MailListPaneLength
{
get => _mailListPaneLength;
set
{
if (SetProperty(ref _mailListPaneLength, value))
{
_configurationService.Set(MailListPaneLengthKey, value);
}
}
}
#endregion
private void UpdateAppCoreWindowTitle()
{
// TODO: WinUI
//var appView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView();
//if (appView != null)
// appView.Title = CoreWindowTitle;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Services.Store;
using Wino.Domain.Interfaces;
using Wino.Domain.Models.Store;
namespace Wino.Shared.WinRT.Services
{
public class StoreManagementService : IStoreManagementService
{
private StoreContext CurrentContext { get; }
private readonly Dictionary<StoreProductType, string> productIds = new Dictionary<StoreProductType, string>()
{
{ StoreProductType.UnlimitedAccounts, "UnlimitedAccounts" }
};
private readonly Dictionary<StoreProductType, string> skuIds = new Dictionary<StoreProductType, string>()
{
{ StoreProductType.UnlimitedAccounts, "9P02MXZ42GSM" }
};
public StoreManagementService()
{
CurrentContext = StoreContext.GetDefault();
}
public async Task<bool> HasProductAsync(StoreProductType productType)
{
var productKey = productIds[productType];
var appLicense = await CurrentContext.GetAppLicenseAsync();
if (appLicense == null)
return false;
// Access the valid licenses for durable add-ons for this app.
foreach (KeyValuePair<string, StoreLicense> item in appLicense.AddOnLicenses)
{
StoreLicense addOnLicense = item.Value;
if (addOnLicense.InAppOfferToken == productKey)
{
return addOnLicense.IsActive;
}
}
return false;
}
public async Task<Domain.Enums.StorePurchaseResult> PurchaseAsync(StoreProductType productType)
{
if (await HasProductAsync(productType))
return Domain.Enums.StorePurchaseResult.AlreadyPurchased;
else
{
var productKey = skuIds[productType];
var result = await CurrentContext.RequestPurchaseAsync(productKey);
switch (result.Status)
{
case StorePurchaseStatus.Succeeded:
return Domain.Enums.StorePurchaseResult.Succeeded;
case StorePurchaseStatus.AlreadyPurchased:
return Domain.Enums.StorePurchaseResult.AlreadyPurchased;
default:
return Domain.Enums.StorePurchaseResult.NotPurchased;
}
}
}
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Services.Store;
using Windows.System;
using Wino.Domain;
using Wino.Domain.Enums;
using Wino.Domain.Interfaces;
namespace Wino.Shared.WinRT.Services
{
public class StoreRatingService : IStoreRatingService
{
private const string RatedStorageKey = nameof(RatedStorageKey);
private const string LatestAskedKey = nameof(LatestAskedKey);
private readonly IConfigurationService _configurationService;
private readonly IDialogService _dialogService;
public StoreRatingService(IConfigurationService configurationService, IDialogService dialogService)
{
_configurationService = configurationService;
_dialogService = dialogService;
}
private void SetRated()
=> _configurationService.SetRoaming(RatedStorageKey, true);
private bool IsAskingThresholdExceeded()
{
var latestAskedDate = _configurationService.Get(LatestAskedKey, DateTime.MinValue);
// Never asked before.
// Set the threshold and wait for the next trigger.
if (latestAskedDate == DateTime.MinValue)
{
_configurationService.Set(LatestAskedKey, DateTime.UtcNow);
}
else if (DateTime.UtcNow >= latestAskedDate.AddMinutes(30))
{
return true;
}
return false;
}
public async Task PromptRatingDialogAsync()
{
// Annoying.
if (Debugger.IsAttached) return;
// Swallow all exceptions. App should not crash in any errors.
try
{
bool isRated = _configurationService.GetRoaming(RatedStorageKey, false);
if (isRated) return;
if (!isRated)
{
if (!IsAskingThresholdExceeded()) return;
var ratingDialogResult = await _dialogService.ShowRatingDialogAsync();
if (ratingDialogResult == null)
return;
if (ratingDialogResult.DontAskAgain)
SetRated();
if (ratingDialogResult.RateWinoClicked)
{
// In case of failure of this call, we will navigate users to Store page directly.
try
{
await ShowPortableRatingDialogAsync();
}
catch (Exception)
{
await Launcher.LaunchUriAsync(new Uri($"ms-windows-store://review/?ProductId=9NCRCVJC50WL"));
}
}
}
}
catch (Exception) { }
finally
{
_configurationService.Set(LatestAskedKey, DateTime.UtcNow);
}
}
private async Task ShowPortableRatingDialogAsync()
{
var _storeContext = StoreContext.GetDefault();
StoreRateAndReviewResult result = await _storeContext.RequestRateAndReviewAppAsync();
// Check status
switch (result.Status)
{
case StoreRateAndReviewStatus.Succeeded:
if (result.WasUpdated)
_dialogService.InfoBarMessage(Translator.Info_ReviewSuccessTitle, Translator.Info_ReviewUpdatedMessage, InfoBarMessageType.Success);
else
_dialogService.InfoBarMessage(Translator.Info_ReviewSuccessTitle, Translator.Info_ReviewNewMessage, InfoBarMessageType.Success);
SetRated();
break;
case StoreRateAndReviewStatus.CanceledByUser:
break;
case StoreRateAndReviewStatus.NetworkError:
_dialogService.InfoBarMessage(Translator.Info_ReviewNetworkErrorTitle, Translator.Info_ReviewNetworkErrorMessage, InfoBarMessageType.Warning);
break;
default:
_dialogService.InfoBarMessage(Translator.Info_ReviewUnknownErrorTitle, string.Format(Translator.Info_ReviewUnknownErrorMessage, result.ExtendedError.Message), InfoBarMessageType.Warning);
break;
}
}
public async Task LaunchStorePageForReviewAsync()
{
try
{
await CoreApplication.GetCurrentView()?.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
{
// TODO: Get it from package info.
await Launcher.LaunchUriAsync(new Uri($"ms-windows-store://review/?ProductId=9NCRCVJC50WL"));
});
}
catch (Exception) { }
}
}
}

View File

@@ -0,0 +1,501 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Windows.Storage;
using Windows.UI.ViewManagement;
using Wino.Domain;
using Wino.Messaging.Client.Shell;
using Wino.Shared.WinRT.Models.Personalization;
using Wino.Shared.WinRT.Extensions;
using Wino.Domain.Models.Personalization;
using Wino.Domain.Interfaces;
using Wino.Domain.Enums;
using Wino.Domain.Exceptions;
#if NET8_0
using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.WinUI.Helpers;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI;
#else
using Windows.UI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Toolkit.Uwp.Helpers;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
#endif
namespace Wino.Shared.WinRT.Services
{
/// <summary>
/// Class providing functionality around switching and restoring theme settings
/// </summary>
public class ThemeService : IThemeService
{
public const string CustomThemeFolderName = "CustomThemes";
private const string MicaThemeId = "a160b1b0-2ab8-4e97-a803-f4050f036e25";
private const string AcrylicThemeId = "fc08e58c-36fd-46e2-a562-26cf277f1467";
private const string CloudsThemeId = "3b621cc2-e270-4a76-8477-737917cccda0";
private const string ForestThemeId = "8bc89b37-a7c5-4049-86e2-de1ae8858dbd";
private const string NightyThemeId = "5b65e04e-fd7e-4c2d-8221-068d3e02d23a";
private const string SnowflakeThemeId = "e143ddde-2e28-4846-9d98-dad63d6505f1";
private const string GardenThemeId = "698e4466-f88c-4799-9c61-f0ea1308ed49";
private Frame mainApplicationFrame;
public event EventHandler<ApplicationElementTheme> ElementThemeChanged;
public event EventHandler<string> AccentColorChanged;
public event EventHandler<string> AccentColorChangedBySystem;
private const string AccentColorKey = nameof(AccentColorKey);
private const string CurrentApplicationThemeKey = nameof(CurrentApplicationThemeKey);
// Custom theme
public const string CustomThemeAccentColorKey = nameof(CustomThemeAccentColorKey);
// Keep reference so it does not get optimized/garbage collected
private readonly UISettings uiSettings = new UISettings();
private readonly IConfigurationService _configurationService;
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IApplicationResourceManager<ResourceDictionary> _applicationResourceManager;
private readonly IAppShellService _appShellService;
private List<AppThemeBase> preDefinedThemes { get; set; } = new List<AppThemeBase>()
{
new SystemAppTheme("Mica", Guid.Parse(MicaThemeId)),
new SystemAppTheme("Acrylic", Guid.Parse(AcrylicThemeId)),
new PreDefinedAppTheme("Nighty", Guid.Parse(NightyThemeId), "#e1b12c", ApplicationElementTheme.Dark),
new PreDefinedAppTheme("Forest", Guid.Parse(ForestThemeId), "#16a085", ApplicationElementTheme.Dark),
new PreDefinedAppTheme("Clouds", Guid.Parse(CloudsThemeId), "#0984e3", ApplicationElementTheme.Light),
new PreDefinedAppTheme("Snowflake", Guid.Parse(SnowflakeThemeId), "#4a69bd", ApplicationElementTheme.Light),
new PreDefinedAppTheme("Garden", Guid.Parse(GardenThemeId), "#05c46b", ApplicationElementTheme.Light),
};
public ThemeService(IConfigurationService configurationService,
IUnderlyingThemeService underlyingThemeService,
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
IAppShellService appShellService)
{
_configurationService = configurationService;
_underlyingThemeService = underlyingThemeService;
_applicationResourceManager = applicationResourceManager;
_appShellService = appShellService;
}
/// <summary>
/// Gets or sets (with LocalSettings persistence) the RequestedTheme of the root element.
/// </summary>
public ApplicationElementTheme RootTheme
{
get => (mainApplicationFrame?.RequestedTheme.ToWinoElementTheme()) ?? ApplicationElementTheme.Default;
set
{
if (mainApplicationFrame == null)
return;
mainApplicationFrame.RequestedTheme = value.ToWindowsElementTheme();
_configurationService.Set(UnderlyingThemeService.SelectedAppThemeKey, value);
UpdateSystemCaptionButtonColors();
// PopupRoot usually needs to react to changes.
NotifyThemeUpdate();
}
}
private Guid currentApplicationThemeId;
public Guid CurrentApplicationThemeId
{
get => currentApplicationThemeId;
set
{
currentApplicationThemeId = value;
_configurationService.Set(CurrentApplicationThemeKey, value);
#if NET8_0
_ = mainApplicationFrame.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
async () => await ApplyCustomThemeAsync(false));
#else
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, async () =>
{
await ApplyCustomThemeAsync(false);
});
#endif
}
}
private string accentColor;
public string AccentColor
{
get => accentColor;
set
{
accentColor = value;
UpdateAccentColor(value);
_configurationService.Set(AccentColorKey, value);
AccentColorChanged?.Invoke(this, value);
}
}
public async Task InitializeAsync()
{
// Already initialized. There is no need.
if (mainApplicationFrame != null)
return;
mainApplicationFrame = _appShellService.AppWindow.Content as Frame;
if (mainApplicationFrame == null) return;
RootTheme = _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
AccentColor = _configurationService.Get(AccentColorKey, string.Empty);
// Set the current theme id. Default to Mica.
var applicationThemeGuid = _configurationService.Get(CurrentApplicationThemeKey, MicaThemeId);
currentApplicationThemeId = Guid.Parse(applicationThemeGuid);
await ApplyCustomThemeAsync(true);
// Registering to color changes, thus we notice when user changes theme system wide
uiSettings.ColorValuesChanged -= UISettingsColorChanged;
uiSettings.ColorValuesChanged += UISettingsColorChanged;
}
private void NotifyThemeUpdate()
{
if (mainApplicationFrame == null) return;
#if NET8_0
_ = mainApplicationFrame.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
ElementThemeChanged?.Invoke(this, RootTheme);
WeakReferenceMessenger.Default.Send(new ApplicationThemeChanged(_underlyingThemeService.IsUnderlyingThemeDark()));
});
#else
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
ElementThemeChanged?.Invoke(this, RootTheme);
WeakReferenceMessenger.Default.Send(new ApplicationThemeChanged(_underlyingThemeService.IsUnderlyingThemeDark()));
});
#endif
}
private void UISettingsColorChanged(UISettings sender, object args)
{
// Make sure we have a reference to our window so we dispatch a UI change
if (mainApplicationFrame != null)
{
// Dispatch on UI thread so that we have a current appbar to access and change
#if NET8_0
_ = mainApplicationFrame.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High,
() =>
{
UpdateSystemCaptionButtonColors();
var accentColor = sender.GetColorValue(UIColorType.Accent);
//AccentColorChangedBySystem?.Invoke(this, accentColor.ToHex());
});
#else
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
UpdateSystemCaptionButtonColors();
var accentColor = sender.GetColorValue(UIColorType.Accent);
//AccentColorChangedBySystem?.Invoke(this, accentColor.ToHex());
});
#endif
}
NotifyThemeUpdate();
}
public void UpdateSystemCaptionButtonColors()
{
if (mainApplicationFrame == null) return;
#if NET8_0
_ = mainApplicationFrame.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal,
() =>
{
var titleBar = _appShellService.AppWindow.AppWindow.TitleBar;
if (titleBar == null) return;
if (_underlyingThemeService.IsUnderlyingThemeDark())
{
titleBar.ButtonForegroundColor = Colors.White;
}
else
{
titleBar.ButtonForegroundColor = Colors.Black;
}
});
#else
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
ApplicationViewTitleBar titleBar = ApplicationView.GetForCurrentView().TitleBar;
if (titleBar == null) return;
if (_underlyingThemeService.IsUnderlyingThemeDark())
{
titleBar.ButtonForegroundColor = Colors.White;
}
else
{
titleBar.ButtonForegroundColor = Colors.Black;
}
});
#endif
}
public void UpdateAccentColor(string hex)
{
// Change accent color if specified.
if (!string.IsNullOrEmpty(hex))
{
#if NET8_0
var brush = new SolidColorBrush(hex.ToColor());
#else
var brush = new SolidColorBrush(Microsoft.Toolkit.Uwp.Helpers.ColorHelper.ToColor(hex));
#endif
if (_applicationResourceManager.ContainsResourceKey("SystemAccentColor"))
_applicationResourceManager.ReplaceResource("SystemAccentColor", brush);
if (_applicationResourceManager.ContainsResourceKey("NavigationViewSelectionIndicatorForeground"))
_applicationResourceManager.ReplaceResource("NavigationViewSelectionIndicatorForeground", brush);
RefreshThemeResource();
}
}
private void RefreshThemeResource()
{
if (mainApplicationFrame == null) return;
if (mainApplicationFrame.RequestedTheme == ElementTheme.Dark)
{
mainApplicationFrame.RequestedTheme = ElementTheme.Light;
mainApplicationFrame.RequestedTheme = ElementTheme.Dark;
}
else if (mainApplicationFrame.RequestedTheme == ElementTheme.Light)
{
mainApplicationFrame.RequestedTheme = ElementTheme.Dark;
mainApplicationFrame.RequestedTheme = ElementTheme.Light;
}
else
{
var isUnderlyingDark = _underlyingThemeService.IsUnderlyingThemeDark();
mainApplicationFrame.RequestedTheme = isUnderlyingDark ? ElementTheme.Light : ElementTheme.Dark;
mainApplicationFrame.RequestedTheme = ElementTheme.Default;
}
}
public async Task ApplyCustomThemeAsync(bool isInitializing)
{
AppThemeBase applyingTheme = null;
var controlThemeList = new List<AppThemeBase>(preDefinedThemes);
// Don't search for custom themes if applying theme is already in pre-defined templates.
// This is important for startup performance because we won't be loading the custom themes on launch.
bool isApplyingPreDefinedTheme = preDefinedThemes.Exists(a => a.Id == currentApplicationThemeId);
if (isApplyingPreDefinedTheme)
{
applyingTheme = preDefinedThemes.Find(a => a.Id == currentApplicationThemeId);
}
else
{
// User applied custom theme. Load custom themes and find it there.
// Fallback to Mica if nothing found.
var customThemes = await GetCurrentCustomThemesAsync();
controlThemeList.AddRange(customThemes.Select(a => new CustomAppTheme(a)));
applyingTheme = controlThemeList.Find(a => a.Id == currentApplicationThemeId) ?? preDefinedThemes.First(a => a.Id == Guid.Parse(MicaThemeId));
}
try
{
var existingThemeDictionary = _applicationResourceManager.GetLastResource();
if (existingThemeDictionary != null && existingThemeDictionary.TryGetValue("ThemeName", out object themeNameString))
{
var themeName = themeNameString.ToString();
// Applying different theme.
if (themeName != applyingTheme.ThemeName)
{
var resourceDictionaryContent = await applyingTheme.GetThemeResourceDictionaryContentAsync();
var resourceDictionary = XamlReader.Load(resourceDictionaryContent) as ResourceDictionary;
// Custom themes require special attention for background image because
// they share the same base theme resource dictionary.
if (applyingTheme is CustomAppTheme)
{
resourceDictionary["ThemeBackgroundImage"] = $"ms-appdata:///local/{CustomThemeFolderName}/{applyingTheme.Id}.jpg";
}
_applicationResourceManager.RemoveResource(existingThemeDictionary);
_applicationResourceManager.AddResource(resourceDictionary);
bool isSystemTheme = applyingTheme is SystemAppTheme || applyingTheme is CustomAppTheme;
if (isSystemTheme)
{
// For system themes, set the RootElement theme from saved values.
// Potential bug: When we set it to system default, theme is not applied when system and
// app element theme is different :)
var savedElement = _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
RootTheme = savedElement;
// Quickly switch theme to apply theme resource changes.
RefreshThemeResource();
}
else
RootTheme = applyingTheme.ForceElementTheme;
// Theme has accent color. Override.
if (!isInitializing)
{
AccentColor = applyingTheme.AccentColor;
}
}
else
UpdateSystemCaptionButtonColors();
}
}
catch (Exception ex)
{
Debug.WriteLine($"Apply theme failed -> {ex.Message}");
}
}
public async Task<List<AppThemeBase>> GetAvailableThemesAsync()
{
var availableThemes = new List<AppThemeBase>(preDefinedThemes);
var customThemes = await GetCurrentCustomThemesAsync();
availableThemes.AddRange(customThemes.Select(a => new CustomAppTheme(a)));
return availableThemes;
}
public async Task<CustomThemeMetadata> CreateNewCustomThemeAsync(string themeName, string accentColor, byte[] wallpaperData)
{
if (wallpaperData == null || wallpaperData.Length == 0)
throw new CustomThemeCreationFailedException(Translator.Exception_CustomThemeMissingWallpaper);
if (string.IsNullOrEmpty(themeName))
throw new CustomThemeCreationFailedException(Translator.Exception_CustomThemeMissingName);
var themes = await GetCurrentCustomThemesAsync();
if (themes.Exists(a => a.Name == themeName))
throw new CustomThemeCreationFailedException(Translator.Exception_CustomThemeExists);
var newTheme = new CustomThemeMetadata()
{
Id = Guid.NewGuid(),
Name = themeName,
AccentColorHex = accentColor
};
// Save wallpaper.
// Filename would be the same as metadata id, in jpg format.
var themeFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(CustomThemeFolderName, CreationCollisionOption.OpenIfExists);
var wallpaperFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.jpg", CreationCollisionOption.ReplaceExisting);
await FileIO.WriteBytesAsync(wallpaperFile, wallpaperData);
// Generate thumbnail for settings page.
var thumbnail = await wallpaperFile.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode.PicturesView);
var thumbnailFile = await themeFolder.CreateFileAsync($"{newTheme.Id}_preview.jpg", CreationCollisionOption.ReplaceExisting);
using (var readerStream = thumbnail.AsStreamForRead())
{
byte[] bytes = new byte[readerStream.Length];
await readerStream.ReadAsync(bytes, 0, bytes.Length);
var buffer = bytes.AsBuffer();
await FileIO.WriteBufferAsync(thumbnailFile, buffer);
}
// Save metadata.
var metadataFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.json", CreationCollisionOption.ReplaceExisting);
var serialized = JsonSerializer.Serialize(newTheme);
await FileIO.WriteTextAsync(metadataFile, serialized);
return newTheme;
}
public async Task<List<CustomThemeMetadata>> GetCurrentCustomThemesAsync()
{
var results = new List<CustomThemeMetadata>();
var themeFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(CustomThemeFolderName, CreationCollisionOption.OpenIfExists);
var allFiles = await themeFolder.GetFilesAsync();
var themeMetadatas = allFiles.Where(a => a.FileType == ".json");
foreach (var theme in themeMetadatas)
{
var metadata = await GetCustomMetadataAsync(theme).ConfigureAwait(false);
if (metadata == null) continue;
results.Add(metadata);
}
return results;
}
private static async Task<CustomThemeMetadata> GetCustomMetadataAsync(IStorageFile file)
{
var fileContent = await FileIO.ReadTextAsync(file);
return JsonSerializer.Deserialize<CustomThemeMetadata>(fileContent);
}
public string GetSystemAccentColorHex()
=> uiSettings.GetColorValue(UIColorType.Accent).ToHex();
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Linq;
using System.Net.Mail;
namespace Wino.Shared.WinRT.Services
{
public static class ThumbnailService
{
private static string[] knownCompanies = new string[]
{
"microsoft.com", "apple.com", "google.com", "steampowered.com", "airbnb.com", "youtube.com", "uber.com"
};
public static bool IsKnown(string mailHost) => !string.IsNullOrEmpty(mailHost) && knownCompanies.Contains(mailHost);
public static string GetHost(string address)
{
if (string.IsNullOrEmpty(address))
return string.Empty;
if (address.Contains('@'))
{
var splitted = address.Split('@');
if (splitted.Length >= 2 && !string.IsNullOrEmpty(splitted[1]))
{
try
{
return new MailAddress(address).Host;
}
catch (Exception)
{
// TODO: Exceptions are ignored for now.
}
}
}
return string.Empty;
}
public static Tuple<bool, string> CheckIsKnown(string host)
{
// Check known hosts.
// Apply company logo if available.
try
{
var last = host.Split('.');
if (last.Length > 2)
host = $"{last[last.Length - 2]}.{last[last.Length - 1]}";
}
catch (Exception)
{
return new Tuple<bool, string>(false, host);
}
return new Tuple<bool, string>(IsKnown(host), host);
}
public static string GetKnownHostImage(string host)
=> $"ms-appx:///Assets/Thumbnails/{host}.png";
}
}

View File

@@ -0,0 +1,40 @@
using Windows.UI.ViewManagement;
using Wino.Domain.Interfaces;
#if NET8_0
using Microsoft.UI.Xaml;
#else
using Microsoft.UI.Xaml;
#endif
namespace Wino.Shared.WinRT.Services
{
public class UnderlyingThemeService : IUnderlyingThemeService
{
public const string SelectedAppThemeKey = nameof(SelectedAppThemeKey);
private readonly UISettings uiSettings = new UISettings();
private readonly IConfigurationService _configurationService;
public UnderlyingThemeService(IConfigurationService configurationService)
{
_configurationService = configurationService;
}
// This should not rely on application window to be present.
// Check theme from the settings, rely on UISettings background color if Default.
public bool IsUnderlyingThemeDark()
{
var currentTheme = _configurationService.Get(SelectedAppThemeKey, ElementTheme.Default);
if (currentTheme == ElementTheme.Default)
return uiSettings.GetColorValue(UIColorType.Background).ToString() == "#FF000000";
else
return currentTheme == ElementTheme.Dark;
}
}
}

View File

@@ -0,0 +1,209 @@
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Metadata;
using Wino.Domain.Enums;
using Wino.Domain.Interfaces;
using Wino.Messaging;
using Wino.Messaging.Enums;
using Wino.Messaging.Server;
namespace Wino.Shared.WinRT.Services
{
public class WinoServerConnectionManager : IWinoServerConnectionManager<AppServiceConnection>
{
private WinoServerConnectionStatus status;
public WinoServerConnectionStatus Status
{
get { return status; }
private set
{
status = value;
StatusChanged?.Invoke(this, value);
}
}
private AppServiceConnection _connection;
public event EventHandler<WinoServerConnectionStatus> StatusChanged;
public AppServiceConnection Connection
{
get { return _connection; }
set
{
if (_connection != null)
{
_connection.RequestReceived -= ServerMessageReceived;
_connection.ServiceClosed -= ServerDisconnected;
}
_connection = value;
if (value == null)
{
Status = WinoServerConnectionStatus.Disconnected;
}
else
{
value.RequestReceived += ServerMessageReceived;
value.ServiceClosed += ServerDisconnected;
Status = WinoServerConnectionStatus.Connected;
}
}
}
public async Task<bool> ConnectAsync()
{
if (Status == WinoServerConnectionStatus.Connected) return true;
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
{
try
{
Status = WinoServerConnectionStatus.Connecting;
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
// If the server connection is success, Status will be updated to Connected by BackgroundActivationHandlerEx.
}
catch (Exception)
{
Status = WinoServerConnectionStatus.Failed;
return false;
}
return true;
}
return false;
}
public async Task<bool> DisconnectAsync()
{
if (Connection == null || Status == WinoServerConnectionStatus.Disconnected) return true;
await Task.CompletedTask;
// TODO: Send disconnect message to the fulltrust process.
return true;
}
public async Task InitializeAsync()
{
var isConnectionSuccessfull = await ConnectAsync();
// TODO: Log connection status
}
private void ServerMessageReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
if (args.Request.Message.TryGetValue(MessageConstants.MessageTypeKey, out object messageTypeObject) && messageTypeObject is int messageTypeInt)
{
var messageType = (MessageType)messageTypeInt;
if (args.Request.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson)
{
switch (messageType)
{
case MessageType.UIMessage:
if (args.Request.Message.TryGetValue(MessageConstants.MessageDataTypeKey, out object dataTypeObject) && dataTypeObject is string dataTypeName)
{
HandleUIMessage(messageJson, dataTypeName);
}
else
throw new ArgumentException("Message data type is missing.");
break;
case MessageType.ServerAction:
HandleServerAction(messageJson);
break;
default:
break;
}
}
}
}
private void HandleServerAction(string messageJson)
{
}
/// <summary>
/// Unpacks IServerMessage objects and delegate it to Messenger for UI to process.
/// </summary>
/// <param name="messageJson">Message data in json format.</param>
private void HandleUIMessage(string messageJson, string typeName)
{
Debug.WriteLine($"C: UImessage ({typeName})");
switch (typeName)
{
case nameof(MailAddedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailAddedMessage>(messageJson));
break;
case nameof(MailDownloadedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailDownloadedMessage>(messageJson));
break;
case nameof(MailRemovedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailRemovedMessage>(messageJson));
break;
case nameof(MailUpdatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailUpdatedMessage>(messageJson));
break;
case nameof(AccountCreatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountCreatedMessage>(messageJson));
break;
case nameof(AccountRemovedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountRemovedMessage>(messageJson));
break;
case nameof(AccountUpdatedMessage):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountUpdatedMessage>(messageJson));
break;
case nameof(DraftCreated):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftCreated>(messageJson));
break;
case nameof(DraftFailed):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftFailed>(messageJson));
break;
case nameof(DraftMapped):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftMapped>(messageJson));
break;
case nameof(FolderRenamed):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<FolderRenamed>(messageJson));
break;
case nameof(FolderSynchronizationEnabled):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<FolderSynchronizationEnabled>(messageJson));
break;
case nameof(MergedInboxRenamed):
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MergedInboxRenamed>(messageJson));
break;
default:
throw new Exception("Invalid data type name passed to client.");
}
}
private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
// TODO: Handle server disconnection.
}
public void DisposeConnection()
{
if (Connection == null) return;
}
public void QueueRequest(IRequestBase request, Guid accountId)
{
// TODO: Queue this request to corresponding account's synchronizer request queue.
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.Extensions.DependencyInjection;
using Windows.ApplicationModel.AppService;
using Wino.Domain.Interfaces;
using Wino.Shared.WinRT.Services;
namespace Wino.Shared.WinRT
{
public static class SharedWinRTContainerSetup
{
public static void RegisterCoreUWPServices(this IServiceCollection services)
{
var serverConnectionManager = new WinoServerConnectionManager();
services.AddSingleton<IWinoServerConnectionManager>(serverConnectionManager);
services.AddSingleton<IWinoServerConnectionManager<AppServiceConnection>>(serverConnectionManager);
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
services.AddSingleton<INativeAppService, NativeAppService>();
services.AddSingleton<IStoreManagementService, StoreManagementService>();
services.AddSingleton<IAppShellService, AppShellService>();
services.AddSingleton<IPreferencesService, PreferencesService>();
services.AddTransient<IConfigurationService, ConfigurationService>();
services.AddTransient<IFileService, FileService>();
services.AddTransient<IStoreRatingService, StoreRatingService>();
services.AddTransient<IKeyPressService, KeyPressService>();
services.AddTransient<INotificationBuilder, NotificationBuilder>();
services.AddTransient<IClipboardService, ClipboardService>();
services.AddSingleton<IThemeService, ThemeService>();
services.AddSingleton<IStatePersistanceService, StatePersistenceService>();
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.UI.Dispatching;
using Wino.Domain.Interfaces;
namespace Wino.Shared.WinRT
{
public class WinAppDispatcher : IDispatcher
{
private readonly DispatcherQueue _dispatcherQueue;
public WinAppDispatcher(DispatcherQueue dispatcherQueue)
{
_dispatcherQueue = dispatcherQueue;
}
public Task ExecuteOnUIThread(Action action) => _dispatcherQueue.EnqueueAsync(() => { action(); });
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>Wino.Shared.WinRT</RootNamespace>
<RuntimeIdentifiers Condition="$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) &gt;= 8">win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers Condition="$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) &lt; 8">win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Properties\**" />
<Content Remove="Properties\**" />
<EmbeddedResource Remove="Properties\**" />
<None Remove="Properties\**" />
<Page Remove="Properties\**" />
<PRIResource Remove="Properties\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.0.240109" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.756" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Domain.csproj" />
<ProjectReference Include="..\Wino.Messaging\Wino.Messaging.csproj" />
</ItemGroup>
</Project>