Cleaning up the solution. Separating Shared.WinRT, Services and Synchronization. Removing synchronization from app. Reducing bundle size by 45mb.
This commit is contained in:
41
Wino.Shared.WinRT/Extensions/ElementThemeExtensions.cs
Normal file
41
Wino.Shared.WinRT/Extensions/ElementThemeExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Wino.Shared.WinRT/Models/Personalization/CustomAppTheme.cs
Normal file
31
Wino.Shared.WinRT/Models/Personalization/CustomAppTheme.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Wino.Shared.WinRT/Models/Personalization/SystemAppTheme.cs
Normal file
13
Wino.Shared.WinRT/Models/Personalization/SystemAppTheme.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
29
Wino.Shared.WinRT/Properties/AssemblyInfo.cs
Normal file
29
Wino.Shared.WinRT/Properties/AssemblyInfo.cs
Normal 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)]
|
||||
33
Wino.Shared.WinRT/Properties/Wino.Core.UWP.rd.xml
Normal file
33
Wino.Shared.WinRT/Properties/Wino.Core.UWP.rd.xml
Normal 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>
|
||||
18
Wino.Shared.WinRT/Services/AppShellService.cs
Normal file
18
Wino.Shared.WinRT/Services/AppShellService.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
19
Wino.Shared.WinRT/Services/ClipboardService.cs
Normal file
19
Wino.Shared.WinRT/Services/ClipboardService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Wino.Shared.WinRT/Services/ConfigurationService.cs
Normal file
46
Wino.Shared.WinRT/Services/ConfigurationService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
38
Wino.Shared.WinRT/Services/FileService.cs
Normal file
38
Wino.Shared.WinRT/Services/FileService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Wino.Shared.WinRT/Services/KeyPressService.cs
Normal file
25
Wino.Shared.WinRT/Services/KeyPressService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
157
Wino.Shared.WinRT/Services/NativeAppService.cs
Normal file
157
Wino.Shared.WinRT/Services/NativeAppService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
231
Wino.Shared.WinRT/Services/NotificationBuilder.cs
Normal file
231
Wino.Shared.WinRT/Services/NotificationBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
193
Wino.Shared.WinRT/Services/PreferencesService.cs
Normal file
193
Wino.Shared.WinRT/Services/PreferencesService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
125
Wino.Shared.WinRT/Services/StatePersistenceService.cs
Normal file
125
Wino.Shared.WinRT/Services/StatePersistenceService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Wino.Shared.WinRT/Services/StoreManagementService.cs
Normal file
73
Wino.Shared.WinRT/Services/StoreManagementService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
Wino.Shared.WinRT/Services/StoreRatingService.cs
Normal file
138
Wino.Shared.WinRT/Services/StoreRatingService.cs
Normal 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
501
Wino.Shared.WinRT/Services/ThemeService.cs
Normal file
501
Wino.Shared.WinRT/Services/ThemeService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
64
Wino.Shared.WinRT/Services/ThumbnailService.cs
Normal file
64
Wino.Shared.WinRT/Services/ThumbnailService.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
40
Wino.Shared.WinRT/Services/UnderlyingThemeService.cs
Normal file
40
Wino.Shared.WinRT/Services/UnderlyingThemeService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
Wino.Shared.WinRT/Services/WinoServerConnectionManager.cs
Normal file
209
Wino.Shared.WinRT/Services/WinoServerConnectionManager.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Wino.Shared.WinRT/SharedWinRTContainerSetup.cs
Normal file
32
Wino.Shared.WinRT/SharedWinRTContainerSetup.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Wino.Shared.WinRT/WinAppDispatcher.cs
Normal file
20
Wino.Shared.WinRT/WinAppDispatcher.cs
Normal 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(); });
|
||||
}
|
||||
}
|
||||
33
Wino.Shared.WinRT/Wino.Shared.WinRT.csproj
Normal file
33
Wino.Shared.WinRT/Wino.Shared.WinRT.csproj
Normal 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)')) >= 8">win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||
<RuntimeIdentifiers Condition="$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)')) < 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>
|
||||
Reference in New Issue
Block a user