Initial commit.

This commit is contained in:
Burak Kaan Köse
2024-04-18 01:44:37 +02:00
parent 524ea4c0e1
commit 12d3814626
671 changed files with 77295 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;
using Wino.Core.Domain.Interfaces;
using Wino.Core.UWP.Services;
using Wino.Services;
namespace Wino.Core.UWP
{
public static class CoreUWPContainerSetup
{
public static void RegisterCoreUWPServices(this IServiceCollection services)
{
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
services.AddSingleton<INativeAppService, NativeAppService>();
services.AddSingleton<IStoreManagementService, StoreManagementService>();
services.AddSingleton<IBackgroundTaskService, BackgroundTaskService>();
services.AddTransient<IAppInitializerService, AppInitializerService>();
services.AddTransient<IConfigurationService, ConfigurationService>();
services.AddTransient<IFileService, FileService>();
services.AddTransient<IStoreRatingService, StoreRatingService>();
services.AddTransient<IKeyPressService, KeyPressService>();
services.AddTransient<IBackgroundSynchronizer, BackgroundSynchronizer>();
services.AddTransient<INotificationBuilder, NotificationBuilder>();
services.AddTransient<IClipboardService, ClipboardService>();
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;
using Windows.UI.Core;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP
{
public class UWPDispatcher : IDispatcher
{
private readonly CoreDispatcher _coreDispatcher;
public UWPDispatcher(CoreDispatcher coreDispatcher)
{
_coreDispatcher = coreDispatcher;
}
public Task ExecuteOnUIThread(Action action)
=> _coreDispatcher.RunAsync(CoreDispatcherPriority.Normal, () => action()).AsTask();
}
}

View File

@@ -0,0 +1,34 @@
using Windows.UI.Xaml;
using Wino.Core.Domain.Enums;
namespace Wino.Core.UWP.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.Core.Domain.Enums;
using Wino.Core.Domain.Models.Personalization;
using Wino.Services;
namespace Wino.Core.UWP.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.Core.Domain.Enums;
using Wino.Core.Domain.Models.Personalization;
namespace Wino.Core.UWP.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.Core.Domain.Enums;
namespace Wino.Core.UWP.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,52 @@
using System;
using System.Threading.Tasks;
using Windows.Storage;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.Services
{
public class AppInitializerService : IAppInitializerService
{
private readonly IBackgroundTaskService _backgroundTaskService;
public AppInitializerService(IBackgroundTaskService backgroundTaskService)
{
_backgroundTaskService = backgroundTaskService;
}
public string GetApplicationDataFolder() => ApplicationData.Current.GetPublisherCacheFolder("WinoShared").Path;
// TODO: Pre 1.7.0 for Wino Calendar...
//public string GetApplicationDataFolder() => ApplicationData.Current.LocalFolder.Path;
public Task MigrateAsync()
{
UnregisterAllBackgroundTasks();
return Task.CompletedTask;
}
#region 1.6.8 -> 1.6.9
private void UnregisterAllBackgroundTasks()
{
_backgroundTaskService.UnregisterAllBackgroundTask();
}
#endregion
#region 1.7.0
/// <summary>
/// We decided to use publisher cache folder as a database going forward.
/// This migration will move the file from application local folder and delete it.
/// Going forward database will be initialized from publisher cache folder.
/// </summary>
private async Task MoveExistingDatabaseToSharedCacheFolderAsync()
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@@ -0,0 +1,144 @@
using System;
using System.Threading.Tasks;
using Serilog;
using Windows.Storage;
using Wino.Core;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Synchronizers;
namespace Wino.Services
{
public interface IBackgroundSynchronizer
{
Task RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason reason);
void CreateLock();
void ReleaseLock();
bool IsBackgroundSynchronizationLocked();
}
/// <summary>
/// Service responsible for handling background synchronization on timer and session connected events.
/// </summary>
public class BackgroundSynchronizer : IBackgroundSynchronizer
{
private const string BackgroundSynchronizationLock = nameof(BackgroundSynchronizationLock);
private readonly IAccountService _accountService;
private readonly IFolderService _folderService;
private readonly IWinoSynchronizerFactory _winoSynchronizerFactory;
public BackgroundSynchronizer(IAccountService accountService,
IFolderService folderService,
IWinoSynchronizerFactory winoSynchronizerFactory)
{
_accountService = accountService;
_folderService = folderService;
_winoSynchronizerFactory = winoSynchronizerFactory;
}
public void CreateLock() => ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] = true;
public void ReleaseLock() => ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] = false;
public bool IsBackgroundSynchronizationLocked()
=> ApplicationData.Current.LocalSettings.Values.ContainsKey(BackgroundSynchronizationLock)
&& ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] is bool boolValue && boolValue;
public async Task RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason reason)
{
Log.Information($"{reason} background synchronization is kicked in.");
// This should never crash.
// We might be in-process or out-of-process.
//if (IsBackgroundSynchronizationLocked())
//{
// Log.Warning("Background synchronization is locked. Hence another background synchronization is canceled.");
// return;
//}
try
{
CreateLock();
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
// We can't sync broken account.
if (account.AttentionReason != AccountAttentionReason.None)
continue;
// TODO
// We can't synchronize without system folder setup is done.
//var isSystemFolderSetupDone = await _folderService.CheckSystemFolderSetupDoneAsync(account.Id);
//// No need to throw here. It's a background process.
//if (!isSystemFolderSetupDone)
// continue;
var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(account.Id);
if (synchronizer.State != AccountSynchronizerState.Idle)
{
Log.Information("Skipping background synchronization for {Name} since current state is {State}", synchronizer.Account.Name, synchronizer.State);
return;
}
await HandleSynchronizationAsync(synchronizer, reason);
}
}
catch (Exception ex)
{
Log.Error($"[BackgroundSynchronization] Failed with message {ex.Message}");
}
finally
{
ReleaseLock();
}
}
private async Task HandleSynchronizationAsync(IBaseSynchronizer synchronizer, BackgroundSynchronizationReason reason)
{
if (synchronizer.State != AccountSynchronizerState.Idle) return;
var account = synchronizer.Account;
try
{
// SessionConnected will do Full synchronization for logon, Timer task will do Inbox only.
var syncType = reason == BackgroundSynchronizationReason.SessionConnected ? SynchronizationType.Full : SynchronizationType.Inbox;
var options = new SynchronizationOptions()
{
AccountId = account.Id,
Type = syncType,
};
await synchronizer.SynchronizeAsync(options);
}
catch (AuthenticationAttentionException authenticationAttentionException)
{
Log.Error(authenticationAttentionException, $"[BackgroundSync] Invalid credentials for account {account.Address}");
account.AttentionReason = AccountAttentionReason.InvalidCredentials;
await _accountService.UpdateAccountAsync(account);
}
catch (SystemFolderConfigurationMissingException configMissingException)
{
Log.Error(configMissingException, $"[BackgroundSync] Missing system folder configuration for account {account.Address}");
account.AttentionReason = AccountAttentionReason.MissingSystemFolderConfiguration;
await _accountService.UpdateAccountAsync(account);
}
catch (Exception ex)
{
Log.Error(ex, "[BackgroundSync] Synchronization failed.");
}
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Serilog;
using Windows.ApplicationModel.Background;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Exceptions;
namespace Wino.Core.UWP.Services
{
public class BackgroundTaskService : IBackgroundTaskService
{
private const string IsBackgroundExecutionDeniedMessageKey = nameof(IsBackgroundExecutionDeniedMessageKey);
public const string BackgroundSynchronizationTimerTaskNameEx = nameof(BackgroundSynchronizationTimerTaskNameEx);
public const string ToastActivationTaskEx = nameof(ToastActivationTaskEx);
private const string SessionConnectedTaskEntryPoint = "Wino.BackgroundTasks.SessionConnectedTask";
private const string SessionConnectedTaskName = "SessionConnectedTask";
private readonly IConfigurationService _configurationService;
private readonly List<string> registeredBackgroundTaskNames = new List<string>();
public BackgroundTaskService(IConfigurationService configurationService)
{
_configurationService = configurationService;
LoadRegisteredTasks();
}
// Calling WinRT all the time for registered tasks might be slow. Cache them on ctor.
private void LoadRegisteredTasks()
{
foreach (var task in BackgroundTaskRegistration.AllTasks)
{
registeredBackgroundTaskNames.Add(task.Value.Name);
}
Log.Information($"Found {registeredBackgroundTaskNames.Count} registered background tasks. [{string.Join(',', registeredBackgroundTaskNames)}]");
}
public async Task HandleBackgroundTaskRegistrations()
{
var response = await BackgroundExecutionManager.RequestAccessAsync();
if (response == BackgroundAccessStatus.DeniedBySystemPolicy ||
response == BackgroundAccessStatus.DeniedByUser)
{
// Only notify users about disabled background execution once.
bool isNotifiedBefore = _configurationService.Get(IsBackgroundExecutionDeniedMessageKey, false);
if (!isNotifiedBefore)
{
_configurationService.Set(IsBackgroundExecutionDeniedMessageKey, true);
throw new BackgroundTaskExecutionRequestDeniedException();
}
}
else
{
RegisterSessionConnectedTask();
RegisterTimerSynchronizationTask();
RegisterToastNotificationHandlerBackgroundTask();
}
}
private bool IsBackgroundTaskRegistered(string taskName)
=> registeredBackgroundTaskNames.Contains(taskName);
public void UnregisterAllBackgroundTask()
{
foreach (var task in BackgroundTaskRegistration.AllTasks)
{
task.Value.Unregister(true);
}
}
private void LogBackgroundTaskRegistration(string taskName)
{
Log.Information($"Registered new background task -> {taskName}");
registeredBackgroundTaskNames.Add($"{taskName}");
}
private BackgroundTaskRegistration RegisterSessionConnectedTask()
{
if (IsBackgroundTaskRegistered(SessionConnectedTaskName)) return null;
var builder = new BackgroundTaskBuilder
{
Name = SessionConnectedTaskName,
TaskEntryPoint = SessionConnectedTaskEntryPoint
};
builder.SetTrigger(new SystemTrigger(SystemTriggerType.SessionConnected, false));
LogBackgroundTaskRegistration(SessionConnectedTaskName);
return builder.Register();
}
private BackgroundTaskRegistration RegisterToastNotificationHandlerBackgroundTask()
{
if (IsBackgroundTaskRegistered(ToastActivationTaskEx)) return null;
var builder = new BackgroundTaskBuilder
{
Name = ToastActivationTaskEx
};
builder.SetTrigger(new ToastNotificationActionTrigger());
LogBackgroundTaskRegistration(ToastActivationTaskEx);
return builder.Register();
}
private BackgroundTaskRegistration RegisterTimerSynchronizationTask()
{
if (IsBackgroundTaskRegistered(BackgroundSynchronizationTimerTaskNameEx)) return null;
var builder = new BackgroundTaskBuilder
{
Name = BackgroundSynchronizationTimerTaskNameEx
};
builder.SetTrigger(new TimeTrigger(15, false));
builder.AddCondition(new SystemCondition(SystemConditionType.InternetAvailable));
LogBackgroundTaskRegistration(BackgroundSynchronizationTimerTaskNameEx);
return builder.Register();
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.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.Core.Domain.Interfaces;
namespace Wino.Core.UWP.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.Core.Domain.Interfaces;
namespace Wino.Core.UWP.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,16 @@
using Windows.System;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.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,131 @@
using System;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.Foundation.Metadata;
using Windows.Security.Authentication.Web;
using Windows.Security.Cryptography;
using Windows.Security.Cryptography.Core;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.System;
using Windows.UI.Shell;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Authorization;
namespace Wino.Services
{
public class NativeAppService : INativeAppService
{
private string _mimeMessagesFolder;
private string _editorBundlePath;
public string GetWebAuthenticationBrokerUri() => WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
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> GetQuillEditorBundlePathAsync()
{
if (string.IsNullOrEmpty(_editorBundlePath))
{
var editorFileFromBundle = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///JS/Quill/full.html"))
.AsTask()
.ConfigureAwait(false);
_editorBundlePath = editorFileFromBundle.Path;
}
return _editorBundlePath;
}
public bool IsAppRunning() => (Window.Current?.Content as Frame)?.Content != null;
public async Task LaunchFileAsync(string filePath)
{
var file = await StorageFile.GetFileFromPathAsync(filePath);
await Launcher.LaunchFileAsync(file);
}
public Task LaunchUriAsync(Uri uri) => Xamarin.Essentials.Launcher.OpenAsync(uri);
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,195 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Notifications;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Services;
namespace Wino.Core.UWP.Services
{
// TODO: Refactor this thing. It's garbage.
public class NotificationBuilder : INotificationBuilder
{
private readonly IUnderlyingThemeService _underlyingThemeService;
private readonly IAccountService _accountService;
private readonly IFolderService _folderService;
public NotificationBuilder(IUnderlyingThemeService underlyingThemeService, IAccountService accountService, IFolderService folderService)
{
_underlyingThemeService = underlyingThemeService;
_accountService = accountService;
_folderService = folderService;
}
public async Task CreateNotificationsAsync(Guid inboxFolderId, IEnumerable<IMailItem> newMailItems)
{
var mailCount = newMailItems.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_MultipleNotificationsTitle, mailCount));
builder.AddButton(GetDismissButton());
builder.Show();
}
else
{
foreach (var mailItem in newMailItems)
{
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 System.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 System.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));
builder.AddButton(GetDeleteButton(mailItem.Id));
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)
=> new ToastButton()
.SetContent(Translator.MailOperation_Delete)
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png"))
.AddArgument(Constants.ToastMailItemIdKey, mailCopyId)
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete)
.SetBackgroundActivation();
private ToastButton GetMarkedAsRead(string mailCopyId)
=> new ToastButton()
.SetContent(Translator.MailOperation_MarkAsRead)
.SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png"))
.AddArgument(Constants.ToastMailItemIdKey, mailCopyId)
.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 (System.Exception ex)
{
// 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,73 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Services.Store;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Store;
namespace Wino.Core.UWP.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,137 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.ApplicationModel.Core;
using Windows.Services.Store;
using Windows.System;
using Wino.Core.Domain;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.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, Domain.Enums.InfoBarMessageType.Success);
else
_dialogService.InfoBarMessage(Translator.Info_ReviewSuccessTitle, Translator.Info_ReviewNewMessage, Domain.Enums.InfoBarMessageType.Success);
SetRated();
break;
case StoreRateAndReviewStatus.CanceledByUser:
break;
case StoreRateAndReviewStatus.NetworkError:
_dialogService.InfoBarMessage(Translator.Info_ReviewNetworkErrorTitle, Translator.Info_ReviewNetworkErrorMessage, Domain.Enums.InfoBarMessageType.Warning);
break;
default:
_dialogService.InfoBarMessage(Translator.Info_ReviewUnknownErrorTitle, string.Format(Translator.Info_ReviewUnknownErrorMessage, result.ExtendedError.Message), Domain.Enums.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,446 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Uwp.Helpers;
using Newtonsoft.Json;
using Windows.Storage;
using Windows.UI;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Personalization;
using Wino.Core.Messages.Shell;
using Wino.Core.UWP.Extensions;
using Wino.Core.UWP.Models.Personalization;
using Wino.Core.UWP.Services;
namespace Wino.Services
{
/// <summary>
/// Class providing functionality around switching and restoring theme settings
/// </summary>
public class ThemeService : IThemeService
{
public const string CustomThemeFolderName = "CustomThemes";
private static string _micaThemeId = "a160b1b0-2ab8-4e97-a803-f4050f036e25";
private static string _acrylicThemeId = "fc08e58c-36fd-46e2-a562-26cf277f1467";
private static string _cloudsThemeId = "3b621cc2-e270-4a76-8477-737917cccda0";
private static string _forestThemeId = "8bc89b37-a7c5-4049-86e2-de1ae8858dbd";
private static string _nightyThemeId = "5b65e04e-fd7e-4c2d-8221-068d3e02d23a";
private static string _snowflakeThemeId = "e143ddde-2e28-4846-9d98-dad63d6505f1";
private static string _gardenThemeId = "698e4466-f88c-4799-9c61-f0ea1308ed49";
private Frame mainApplicationFrame = null;
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 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)
{
_configurationService = configurationService;
_underlyingThemeService = underlyingThemeService;
_applicationResourceManager = applicationResourceManager;
}
/// <summary>
/// Gets or sets (with LocalSettings persistence) the RequestedTheme of the root element.
/// </summary>
public ApplicationElementTheme RootTheme
{
get
{
if (mainApplicationFrame == null) return ApplicationElementTheme.Default;
return mainApplicationFrame.RequestedTheme.ToWinoElementTheme();
}
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 { return currentApplicationThemeId; }
set
{
currentApplicationThemeId = value;
_configurationService.Set(CurrentApplicationThemeKey, value);
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, async () =>
{
await ApplyCustomThemeAsync(false);
});
}
}
private string accentColor;
public string AccentColor
{
get { return 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;
// Save reference as this might be null when the user is in another app
mainApplicationFrame = Window.Current.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;
}
private void NotifyThemeUpdate()
{
if (mainApplicationFrame == null || mainApplicationFrame.Dispatcher == null) return;
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
ElementThemeChanged?.Invoke(this, RootTheme);
WeakReferenceMessenger.Default.Send(new ApplicationThemeChanged(_underlyingThemeService.IsUnderlyingThemeDark()));
});
}
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
_ = mainApplicationFrame.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () =>
{
UpdateSystemCaptionButtonColors();
var accentColor = sender.GetColorValue(UIColorType.Accent);
//AccentColorChangedBySystem?.Invoke(this, accentColor.ToHex());
});
}
NotifyThemeUpdate();
}
public void UpdateSystemCaptionButtonColors()
{
if (mainApplicationFrame == null) return;
_ = 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;
}
});
}
public void UpdateAccentColor(string hex)
{
// Change accent color if specified.
if (!string.IsNullOrEmpty(hex))
{
var brush = new SolidColorBrush(Microsoft.Toolkit.Uwp.Helpers.ColorHelper.ToColor(hex));
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 = JsonConvert.SerializeObject(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 async Task<CustomThemeMetadata> GetCustomMetadataAsync(IStorageFile file)
{
var fileContent = await FileIO.ReadTextAsync(file);
return JsonConvert.DeserializeObject<CustomThemeMetadata>(fileContent);
}
public string GetSystemAccentColorHex()
=> uiSettings.GetColorValue(UIColorType.Accent).ToHex();
}
}

View File

@@ -0,0 +1,32 @@
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.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,184 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{395F19BA-1E42-495C-9DB5-1A6F537FCCB8}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Wino.Core.UWP</RootNamespace>
<AssemblyName>Wino.Core.UWP</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
<TargetPlatformVersion Condition=" '$(TargetPlatformVersion)' == '' ">10.0.22621.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x86\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
<PlatformTarget>x86</PlatformTarget>
<OutputPath>bin\x86\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
<PlatformTarget>ARM</PlatformTarget>
<OutputPath>bin\ARM\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM64'">
<PlatformTarget>ARM64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\ARM64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM64'">
<PlatformTarget>ARM64</PlatformTarget>
<OutputPath>bin\ARM64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\x64\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<NoWarn>;2008</NoWarn>
<DebugType>full</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
<PlatformTarget>x64</PlatformTarget>
<OutputPath>bin\x64\Release\</OutputPath>
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
<Optimize>true</Optimize>
<NoWarn>;2008</NoWarn>
<DebugType>pdbonly</DebugType>
<UseVSHostingProcess>false</UseVSHostingProcess>
<ErrorReport>prompt</ErrorReport>
</PropertyGroup>
<PropertyGroup>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="CoreUWPContainerSetup.cs" />
<Compile Include="Dispatcher.cs" />
<Compile Include="Extensions\ElementThemeExtensions.cs" />
<Compile Include="Models\Personalization\CustomAppTheme.cs" />
<Compile Include="Models\Personalization\PreDefinedAppTheme.cs" />
<Compile Include="Models\Personalization\SystemAppTheme.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\AppInitializerService.cs" />
<Compile Include="Services\BackgroundSynchronizer.cs" />
<Compile Include="Services\BackgroundTaskService.cs" />
<Compile Include="Services\ClipboardService.cs" />
<Compile Include="Services\ConfigurationService.cs" />
<Compile Include="Services\FileService.cs" />
<Compile Include="Services\KeyPressService.cs" />
<Compile Include="Services\NativeAppService.cs" />
<Compile Include="Services\NotificationBuilder.cs" />
<Compile Include="Services\StoreManagementService.cs" />
<Compile Include="Services\StoreRatingService.cs" />
<Compile Include="Services\ThemeService.cs" />
<Compile Include="Services\UnderlyingThemeService.cs" />
<EmbeddedResource Include="Properties\Wino.Core.UWP.rd.xml" />
</ItemGroup>
<ItemGroup>
<!--<PackageReference Include="CommunityToolkit.Uwp.Helpers">
<Version>8.0.230907</Version>
</PackageReference>-->
<PackageReference Include="Microsoft.AppCenter.Analytics">
<Version>5.0.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.14</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp">
<Version>7.1.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications">
<Version>7.1.3</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj">
<Project>{cf3312e5-5da0-4867-9945-49ea7598af1f}</Project>
<Name>Wino.Core.Domain</Name>
</ProjectReference>
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj">
<Project>{e6b1632a-8901-41e8-9ddf-6793c7698b0b}</Project>
<Name>Wino.Core</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup />
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '14.0' ">
<VisualStudioVersion>14.0</VisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>