Initial commit.
This commit is contained in:
27
Wino.Core.UWP/CoreUWPContainerSetup.cs
Normal file
27
Wino.Core.UWP/CoreUWPContainerSetup.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Wino.Core.UWP/Dispatcher.cs
Normal file
20
Wino.Core.UWP/Dispatcher.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
34
Wino.Core.UWP/Extensions/ElementThemeExtensions.cs
Normal file
34
Wino.Core.UWP/Extensions/ElementThemeExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Wino.Core.UWP/Models/Personalization/CustomAppTheme.cs
Normal file
31
Wino.Core.UWP/Models/Personalization/CustomAppTheme.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Wino.Core.UWP/Models/Personalization/PreDefinedAppTheme.cs
Normal file
34
Wino.Core.UWP/Models/Personalization/PreDefinedAppTheme.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Wino.Core.UWP/Models/Personalization/SystemAppTheme.cs
Normal file
13
Wino.Core.UWP/Models/Personalization/SystemAppTheme.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
29
Wino.Core.UWP/Properties/AssemblyInfo.cs
Normal file
29
Wino.Core.UWP/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.Core.UWP/Properties/Wino.Core.UWP.rd.xml
Normal file
33
Wino.Core.UWP/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>
|
||||
52
Wino.Core.UWP/Services/AppInitializerService.cs
Normal file
52
Wino.Core.UWP/Services/AppInitializerService.cs
Normal 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
|
||||
}
|
||||
}
|
||||
144
Wino.Core.UWP/Services/BackgroundSynchronizer.cs
Normal file
144
Wino.Core.UWP/Services/BackgroundSynchronizer.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
Wino.Core.UWP/Services/BackgroundTaskService.cs
Normal file
136
Wino.Core.UWP/Services/BackgroundTaskService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Wino.Core.UWP/Services/ClipboardService.cs
Normal file
19
Wino.Core.UWP/Services/ClipboardService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Wino.Core.UWP/Services/ConfigurationService.cs
Normal file
46
Wino.Core.UWP/Services/ConfigurationService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
38
Wino.Core.UWP/Services/FileService.cs
Normal file
38
Wino.Core.UWP/Services/FileService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Wino.Core.UWP/Services/KeyPressService.cs
Normal file
16
Wino.Core.UWP/Services/KeyPressService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
131
Wino.Core.UWP/Services/NativeAppService.cs
Normal file
131
Wino.Core.UWP/Services/NativeAppService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
195
Wino.Core.UWP/Services/NotificationBuilder.cs
Normal file
195
Wino.Core.UWP/Services/NotificationBuilder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Wino.Core.UWP/Services/StoreManagementService.cs
Normal file
73
Wino.Core.UWP/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.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
137
Wino.Core.UWP/Services/StoreRatingService.cs
Normal file
137
Wino.Core.UWP/Services/StoreRatingService.cs
Normal 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
446
Wino.Core.UWP/Services/ThemeService.cs
Normal file
446
Wino.Core.UWP/Services/ThemeService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
32
Wino.Core.UWP/Services/UnderlyingThemeService.cs
Normal file
32
Wino.Core.UWP/Services/UnderlyingThemeService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
184
Wino.Core.UWP/Wino.Core.UWP.csproj
Normal file
184
Wino.Core.UWP/Wino.Core.UWP.csproj
Normal 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)' < '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>
|
||||
Reference in New Issue
Block a user