Merge core project into winui project.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
using System.Linq;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.WinUI;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ApplicationResourceManager : IApplicationResourceManager<ResourceDictionary>
|
||||
{
|
||||
public void AddResource(ResourceDictionary resource)
|
||||
=> WinoApplication.Current.Resources.MergedDictionaries.Add(resource);
|
||||
public void RemoveResource(ResourceDictionary resource)
|
||||
=> WinoApplication.Current.Resources.MergedDictionaries.Remove(resource);
|
||||
|
||||
public bool ContainsResourceKey(string resourceKey)
|
||||
=> WinoApplication.Current.Resources.ContainsKey(resourceKey);
|
||||
|
||||
public ResourceDictionary GetLastResource()
|
||||
=> WinoApplication.Current.Resources.MergedDictionaries.LastOrDefault();
|
||||
|
||||
public void ReplaceResource(string resourceKey, object resource)
|
||||
=> WinoApplication.Current.Resources[resourceKey] = resource;
|
||||
|
||||
public TReturnType GetResource<TReturnType>(string resourceKey)
|
||||
=> (TReturnType)WinoApplication.Current.Resources[resourceKey];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class ClipboardService : IClipboardService
|
||||
{
|
||||
public Task CopyClipboardAsync(string text)
|
||||
{
|
||||
var package = new DataPackage();
|
||||
package.SetText(text);
|
||||
|
||||
Clipboard.SetContent(package);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using Windows.Foundation.Collections;
|
||||
using Windows.Storage;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.WinUI.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 static T GetInternal<T>(string key, IPropertySet collection, T defaultValue = default)
|
||||
{
|
||||
if (collection.TryGetValue(key, out object value))
|
||||
{
|
||||
var stringValue = value?.ToString();
|
||||
|
||||
if (typeof(T).IsEnum)
|
||||
return (T)Enum.Parse(typeof(T), stringValue);
|
||||
|
||||
if ((typeof(T) == typeof(Guid?) || typeof(T) == typeof(Guid)) && Guid.TryParse(stringValue, out Guid guidResult))
|
||||
{
|
||||
return (T)(object)guidResult;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(TimeSpan))
|
||||
{
|
||||
return (T)(object)TimeSpan.Parse(stringValue);
|
||||
}
|
||||
|
||||
return (T)Convert.ChangeType(stringValue, typeof(T));
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static void SetInternal(string key, object value, IPropertySet collection)
|
||||
=> collection[key] = value?.ToString();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,8 +14,8 @@ using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.WinUI.Extensions;
|
||||
using Wino.Core.WinUI.Services;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
using Wino.Dialogs;
|
||||
using Wino.Mail.Dialogs;
|
||||
using Wino.Messaging.Server;
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Serilog;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.AccessCache;
|
||||
using Windows.Storage.Pickers;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Common;
|
||||
using Wino.Core.Domain.Models.Printing;
|
||||
using Wino.Mail.WinUI.Dialogs;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
using Wino.Dialogs;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
using WinRT.Interop;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class DialogServiceBase : IDialogServiceBase
|
||||
{
|
||||
private SemaphoreSlim _presentationSemaphore = new SemaphoreSlim(1);
|
||||
|
||||
protected INewThemeService ThemeService { get; }
|
||||
protected IConfigurationService ConfigurationService { get; }
|
||||
|
||||
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
||||
|
||||
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
||||
{
|
||||
ThemeService = themeService;
|
||||
ConfigurationService = configurationService;
|
||||
ApplicationResourceManager = applicationResourceManager;
|
||||
}
|
||||
|
||||
protected XamlRoot GetXamlRoot()
|
||||
{
|
||||
return WinoApplication.MainWindow?.Content?.XamlRoot;
|
||||
}
|
||||
|
||||
public async Task<string> PickFilePathAsync(string saveFileName)
|
||||
{
|
||||
var picker = new FolderPicker()
|
||||
{
|
||||
SuggestedStartLocation = PickerLocationId.Desktop
|
||||
};
|
||||
|
||||
picker.FileTypeFilter.Add("*");
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var folder = await picker.PickSingleFolderAsync();
|
||||
if (folder == null) return string.Empty;
|
||||
|
||||
StorageApplicationPermissions.FutureAccessList.Add(folder);
|
||||
|
||||
return folder.Path;
|
||||
|
||||
//var picker = new FileSavePicker
|
||||
//{
|
||||
// SuggestedStartLocation = PickerLocationId.Desktop,
|
||||
// SuggestedFileName = saveFileName
|
||||
//};
|
||||
|
||||
//picker.FileTypeChoices.Add(Translator.FilteringOption_All, [".*"]);
|
||||
|
||||
//var file = await picker.PickSaveFileAsync();
|
||||
//if (file == null) return string.Empty;
|
||||
|
||||
//StorageApplicationPermissions.FutureAccessList.Add(file);
|
||||
|
||||
//return file.Path;
|
||||
}
|
||||
|
||||
public async Task<List<SharedFile>> PickFilesAsync(params object[] typeFilters)
|
||||
{
|
||||
var returnList = new List<SharedFile>();
|
||||
var picker = new FileOpenPicker
|
||||
{
|
||||
ViewMode = PickerViewMode.Thumbnail,
|
||||
SuggestedStartLocation = PickerLocationId.Desktop
|
||||
};
|
||||
|
||||
foreach (var filter in typeFilters)
|
||||
{
|
||||
picker.FileTypeFilter.Add(filter.ToString());
|
||||
}
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var files = await picker.PickMultipleFilesAsync();
|
||||
if (files == null) return returnList;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
StorageApplicationPermissions.FutureAccessList.Add(file);
|
||||
|
||||
var sharedFile = await file.ToSharedFileAsync();
|
||||
returnList.Add(sharedFile);
|
||||
}
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
private async Task<StorageFile> PickFileAsync(params object[] typeFilters)
|
||||
{
|
||||
var picker = new FileOpenPicker
|
||||
{
|
||||
ViewMode = PickerViewMode.Thumbnail
|
||||
};
|
||||
|
||||
foreach (var filter in typeFilters)
|
||||
{
|
||||
picker.FileTypeFilter.Add(filter.ToString());
|
||||
}
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var file = await picker.PickSingleFileAsync();
|
||||
|
||||
if (file == null) return null;
|
||||
|
||||
StorageApplicationPermissions.FutureAccessList.Add(file);
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
public virtual IAccountCreationDialog GetAccountCreationDialog(AccountCreationDialogResult accountCreationDialogResult)
|
||||
{
|
||||
return new AccountCreationDialog
|
||||
{
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
|
||||
XamlRoot = GetXamlRoot()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<byte[]> PickWindowsFileContentAsync(params object[] typeFilters)
|
||||
{
|
||||
var file = await PickFileAsync(typeFilters);
|
||||
|
||||
if (file == null) return [];
|
||||
|
||||
return await file.ToByteArrayAsync();
|
||||
}
|
||||
|
||||
public Task ShowMessageAsync(string message, string title, WinoCustomMessageDialogIcon icon = WinoCustomMessageDialogIcon.Information)
|
||||
=> ShowWinoCustomMessageDialogAsync(title, message, Translator.Buttons_Close, icon);
|
||||
|
||||
public Task<bool> ShowConfirmationDialogAsync(string question, string title, string confirmationButtonTitle)
|
||||
=> ShowWinoCustomMessageDialogAsync(title, question, confirmationButtonTitle, WinoCustomMessageDialogIcon.Question, Translator.Buttons_Cancel, string.Empty);
|
||||
|
||||
public async Task<bool> ShowWinoCustomMessageDialogAsync(string title,
|
||||
string description,
|
||||
string approveButtonText,
|
||||
WinoCustomMessageDialogIcon? icon,
|
||||
string cancelButtonText = "",
|
||||
string dontAskAgainConfigurationKey = "")
|
||||
|
||||
{
|
||||
// This config key has been marked as don't ask again already.
|
||||
// Return immidiate result without presenting the dialog.
|
||||
|
||||
bool isDontAskEnabled = !string.IsNullOrEmpty(dontAskAgainConfigurationKey);
|
||||
|
||||
if (isDontAskEnabled && ConfigurationService.Get(dontAskAgainConfigurationKey, false)) return false;
|
||||
|
||||
var informationContainer = new CustomMessageDialogInformationContainer(title, description, icon.Value, isDontAskEnabled);
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Style = ApplicationResourceManager.GetResource<Style>("WinoDialogStyle"),
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
|
||||
DefaultButton = ContentDialogButton.Primary,
|
||||
PrimaryButtonText = approveButtonText,
|
||||
ContentTemplate = ApplicationResourceManager.GetResource<DataTemplate>("CustomWinoContentDialogContentTemplate"),
|
||||
Content = informationContainer
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(cancelButtonText))
|
||||
{
|
||||
dialog.SecondaryButtonText = cancelButtonText;
|
||||
}
|
||||
|
||||
var dialogResult = await HandleDialogPresentationAsync(dialog);
|
||||
|
||||
// Mark this key to not ask again if user checked the checkbox.
|
||||
if (informationContainer.IsDontAskChecked)
|
||||
{
|
||||
ConfigurationService.Set(dontAskAgainConfigurationKey, true);
|
||||
}
|
||||
|
||||
return dialogResult == ContentDialogResult.Primary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for PopupRoot to be available before presenting the dialog and returns the result after presentation.
|
||||
/// </summary>
|
||||
/// <param name="dialog">Dialog to present and wait for closing.</param>
|
||||
/// <returns>Dialog result from WinRT.</returns>
|
||||
public async Task<ContentDialogResult> HandleDialogPresentationAsync(ContentDialog dialog)
|
||||
{
|
||||
await _presentationSemaphore.WaitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
dialog.XamlRoot = GetXamlRoot();
|
||||
|
||||
return await dialog.ShowAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Handling dialog service failed. Dialog was {dialog.GetType().Name}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_presentationSemaphore.Release();
|
||||
}
|
||||
|
||||
return ContentDialogResult.None;
|
||||
}
|
||||
|
||||
|
||||
public void InfoBarMessage(string title, string message, InfoBarMessageType messageType)
|
||||
=> WeakReferenceMessenger.Default.Send(new InfoBarMessageRequested(messageType, title, message));
|
||||
|
||||
public void InfoBarMessage(string title, string message, InfoBarMessageType messageType, string actionButtonText, Action action)
|
||||
=> WeakReferenceMessenger.Default.Send(new InfoBarMessageRequested(messageType, title, message, actionButtonText, action));
|
||||
|
||||
public void ShowNotSupportedMessage()
|
||||
=> InfoBarMessage(Translator.Info_UnsupportedFunctionalityTitle,
|
||||
Translator.Info_UnsupportedFunctionalityDescription,
|
||||
InfoBarMessageType.Error);
|
||||
|
||||
public async Task<string> ShowTextInputDialogAsync(string currentInput, string dialogTitle, string dialogDescription, string primaryButtonText)
|
||||
{
|
||||
var inputDialog = new TextInputDialog()
|
||||
{
|
||||
CurrentInput = currentInput,
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
|
||||
Title = dialogTitle
|
||||
};
|
||||
|
||||
inputDialog.SetDescription(dialogDescription);
|
||||
inputDialog.SetPrimaryButtonText(primaryButtonText);
|
||||
|
||||
await HandleDialogPresentationAsync(inputDialog);
|
||||
|
||||
if (inputDialog.HasInput.GetValueOrDefault() && !currentInput.Equals(inputDialog.CurrentInput))
|
||||
return inputDialog.CurrentInput;
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task<string> PickWindowsFolderAsync()
|
||||
{
|
||||
var picker = new FolderPicker
|
||||
{
|
||||
SuggestedStartLocation = PickerLocationId.DocumentsLibrary
|
||||
};
|
||||
|
||||
picker.FileTypeFilter.Add("*");
|
||||
|
||||
nint windowHandle = WindowNative.GetWindowHandle(WinoApplication.MainWindow);
|
||||
InitializeWithWindow.Initialize(picker, windowHandle);
|
||||
|
||||
var pickedFolder = await picker.PickSingleFolderAsync();
|
||||
|
||||
if (pickedFolder != null)
|
||||
{
|
||||
Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.AddOrReplace("FolderPickerToken", pickedFolder);
|
||||
|
||||
return pickedFolder.Path;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task<bool> ShowCustomThemeBuilderDialogAsync()
|
||||
{
|
||||
var themeBuilderDialog = new CustomThemeBuilderDialog()
|
||||
{
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||
};
|
||||
|
||||
var dialogResult = await HandleDialogPresentationAsync(themeBuilderDialog);
|
||||
|
||||
return dialogResult == ContentDialogResult.Primary;
|
||||
}
|
||||
|
||||
public async Task<AccountCreationDialogResult> ShowAccountProviderSelectionDialogAsync(List<IProviderDetail> availableProviders)
|
||||
{
|
||||
var dialog = new NewAccountDialog
|
||||
{
|
||||
Providers = availableProviders,
|
||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||
};
|
||||
|
||||
await HandleDialogPresentationAsync(dialog);
|
||||
|
||||
return dialog.Result;
|
||||
}
|
||||
|
||||
public async Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create the print dialog
|
||||
var dialog = initialSettings != null
|
||||
? new PrintDialog(initialSettings)
|
||||
: new PrintDialog();
|
||||
|
||||
// Set the XamlRoot for proper display
|
||||
dialog.XamlRoot = GetXamlRoot();
|
||||
|
||||
// Get available printers asynchronously when the dialog is loaded
|
||||
dialog.Loaded += async (sender, e) =>
|
||||
{
|
||||
await dialog.LoadAvailablePrintersAsync();
|
||||
};
|
||||
|
||||
// Show the dialog
|
||||
var result = await HandleDialogPresentationAsync(dialog);
|
||||
|
||||
// Return the settings if user clicked Print, otherwise null
|
||||
return result == ContentDialogResult.Primary
|
||||
? dialog.PrintSettings
|
||||
: null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception if logging is available
|
||||
Log.Error(ex, "Error showing print dialog");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Storage;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.WinUI.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();
|
||||
}
|
||||
|
||||
public async Task<bool> SaveLogsToFolderAsync(string logsFolder, string destinationFolder)
|
||||
{
|
||||
var logFiles = Directory.GetFiles(logsFolder, "*.log");
|
||||
|
||||
if (logFiles.Length == 0) return false;
|
||||
|
||||
using var fileStream = await GetFileStreamAsync(destinationFolder, Constants.LogArchiveFileName);
|
||||
using var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true);
|
||||
|
||||
foreach (var logFile in logFiles)
|
||||
{
|
||||
using FileStream logFileStream = File.Open(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
|
||||
var zipArchiveEntry = archive.CreateEntry(Path.GetFileName(logFile), CompressionLevel.Fastest);
|
||||
using var zipStream = zipArchiveEntry.Open();
|
||||
|
||||
await logFileStream.CopyToAsync(zipStream);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.UI.Input;
|
||||
using Windows.System;
|
||||
using Windows.UI.Core;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class KeyPressService : IKeyPressService
|
||||
{
|
||||
public bool IsCtrlKeyPressed()
|
||||
=> InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
public bool IsShiftKeyPressed()
|
||||
=> InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Storage;
|
||||
using Windows.System;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
|
||||
|
||||
#if WINDOWS_UWP
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
#endif
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class NativeAppService : INativeAppService
|
||||
{
|
||||
private string _mimeMessagesFolder;
|
||||
private string _editorBundlePath;
|
||||
|
||||
public Func<IntPtr> GetCoreWindowHwnd { get; set; }
|
||||
|
||||
public string GetWebAuthenticationBrokerUri()
|
||||
{
|
||||
#if WINDOWS_UWP
|
||||
return WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
|
||||
#endif
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<string> GetEditorBundlePathAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_editorBundlePath))
|
||||
{
|
||||
var editorFileFromBundle = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///JS/editor.html"))
|
||||
.AsTask()
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_editorBundlePath = editorFileFromBundle.Path;
|
||||
}
|
||||
|
||||
return _editorBundlePath;
|
||||
}
|
||||
|
||||
[Obsolete("This should be removed. There should be no functionality.")]
|
||||
public bool IsAppRunning()
|
||||
{
|
||||
#if WINDOWS_UWP
|
||||
return (Window.Current?.Content as Frame)?.Content != null;
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public async Task LaunchFileAsync(string filePath)
|
||||
{
|
||||
var file = await StorageFile.GetFileFromPathAsync(filePath);
|
||||
|
||||
await Launcher.LaunchFileAsync(file);
|
||||
}
|
||||
|
||||
public Task<bool> LaunchUriAsync(Uri uri) => Launcher.LaunchUriAsync(uri).AsTask();
|
||||
|
||||
public string GetFullAppVersion()
|
||||
{
|
||||
Package package = Package.Current;
|
||||
PackageId packageId = package.Id;
|
||||
PackageVersion version = packageId.Version;
|
||||
|
||||
return string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision);
|
||||
}
|
||||
|
||||
[Obsolete("Not supported for Win SDK")]
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -6,9 +6,9 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.WinUI;
|
||||
using Wino.Core.WinUI.Interfaces;
|
||||
using Wino.Core.WinUI.Services;
|
||||
using Wino.Mail.WinUI;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
using Wino.Helpers;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Messages;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class NavigationServiceBase
|
||||
{
|
||||
public NavigationTransitionInfo GetNavigationTransitionInfo(NavigationTransitionType transition)
|
||||
{
|
||||
return transition switch
|
||||
{
|
||||
NavigationTransitionType.DrillIn => new DrillInNavigationTransitionInfo(),
|
||||
NavigationTransitionType.Entrance => new EntranceNavigationTransitionInfo(),
|
||||
_ => new SuppressNavigationTransitionInfo(),
|
||||
};
|
||||
}
|
||||
|
||||
public Type GetCurrentFrameType(ref Frame _frame)
|
||||
{
|
||||
if (_frame != null && _frame.Content != null)
|
||||
return _frame.Content.GetType();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.Storage;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Personalization;
|
||||
using Wino.Mail.WinUI;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Models.Personalization;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
using Wino.Messaging.Client.Shell;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Next-generation theme service with enhanced WinUI support including backdrop management
|
||||
/// </summary>
|
||||
public class NewThemeService : INewThemeService
|
||||
{
|
||||
public const string CustomThemeFolderName = "CustomThemes";
|
||||
|
||||
private static string _defaultThemeId = "00000000-0000-0000-0000-000000000000";
|
||||
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";
|
||||
|
||||
public event EventHandler<ApplicationElementTheme> ElementThemeChanged;
|
||||
public event EventHandler<string> AccentColorChanged;
|
||||
public event EventHandler<WindowBackdropType> BackdropChanged;
|
||||
|
||||
private const string AccentColorKey = nameof(AccentColorKey);
|
||||
private const string CurrentApplicationThemeKey = nameof(CurrentApplicationThemeKey);
|
||||
private const string WindowBackdropTypeKey = nameof(WindowBackdropTypeKey);
|
||||
|
||||
// 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("Default", Guid.Parse(_defaultThemeId)),
|
||||
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 NewThemeService(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
|
||||
{
|
||||
return GetShellRootContent().RequestedTheme.ToWinoElementTheme();
|
||||
}
|
||||
set
|
||||
{
|
||||
GetShellRootContent().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);
|
||||
|
||||
if (WinoApplication.MainWindow != null)
|
||||
{
|
||||
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(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);
|
||||
}
|
||||
}
|
||||
|
||||
private WindowBackdropType currentBackdropType;
|
||||
|
||||
public WindowBackdropType CurrentBackdropType
|
||||
{
|
||||
get { return currentBackdropType; }
|
||||
set
|
||||
{
|
||||
// Only update if the backdrop type has actually changed
|
||||
if (currentBackdropType == value) return;
|
||||
|
||||
currentBackdropType = value;
|
||||
_configurationService.Set(WindowBackdropTypeKey, (int)value);
|
||||
|
||||
if (WinoApplication.MainWindow != null)
|
||||
{
|
||||
WinoApplication.MainWindow.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ApplyBackdrop(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCustomTheme
|
||||
{
|
||||
get
|
||||
{
|
||||
// If no theme is set, it's not a custom theme
|
||||
if (currentApplicationThemeId == null) return false;
|
||||
|
||||
// Check if current theme is not in predefined themes (all themes now are custom or predefined, no system themes)
|
||||
return !preDefinedThemes.Exists(a => a.Id == currentApplicationThemeId);
|
||||
}
|
||||
}
|
||||
|
||||
public FrameworkElement GetShellRootContent() => (WinoApplication.MainWindow as IWinoShellWindow)?.GetRootContent() ?? throw new Exception("No root content found");
|
||||
|
||||
private bool isInitialized = false;
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Already initialized. There is no need.
|
||||
if (isInitialized) return;
|
||||
|
||||
RootTheme = _configurationService.Get(UnderlyingThemeService.SelectedAppThemeKey, ApplicationElementTheme.Default);
|
||||
AccentColor = _configurationService.Get(AccentColorKey, string.Empty);
|
||||
|
||||
// Set the current theme id. Don't set a default for backward compatibility.
|
||||
var storedThemeId = _configurationService.Get<Guid?>(CurrentApplicationThemeKey, null);
|
||||
currentApplicationThemeId = storedThemeId;
|
||||
|
||||
// Load backdrop setting, default to Mica
|
||||
currentBackdropType = (WindowBackdropType)_configurationService.Get(WindowBackdropTypeKey, (int)WindowBackdropType.Mica);
|
||||
|
||||
// Apply backdrop first, then theme
|
||||
ApplyBackdrop(currentBackdropType);
|
||||
await ApplyCustomThemeAsync(true);
|
||||
|
||||
// Registering to color changes, thus we notice when user changes theme system wide
|
||||
|
||||
// TODO: WinUI: This event seems to be very unreliable. It causes a crash when the function runs under.
|
||||
//uiSettings.ColorValuesChanged -= UISettingsColorChanged;
|
||||
//uiSettings.ColorValuesChanged += UISettingsColorChanged;
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
public void ApplyBackdrop(WindowBackdropType backdropType)
|
||||
{
|
||||
if (WinoApplication.MainWindow is not WindowEx windowEx)
|
||||
{
|
||||
Debug.WriteLine("MainWindow is not WindowEx, cannot apply backdrop");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Microsoft.UI.Xaml.Media.SystemBackdrop backdrop = backdropType switch
|
||||
{
|
||||
WindowBackdropType.Mica => new MicaBackdrop() { Kind = Microsoft.UI.Composition.SystemBackdrops.MicaKind.Base },
|
||||
WindowBackdropType.MicaAlt => new MicaBackdrop() { Kind = Microsoft.UI.Composition.SystemBackdrops.MicaKind.BaseAlt },
|
||||
WindowBackdropType.DesktopAcrylic => new DesktopAcrylicBackdrop(),
|
||||
WindowBackdropType.AcrylicBase => new DesktopAcrylicBackdrop(), // Using DesktopAcrylic as base
|
||||
WindowBackdropType.AcrylicThin => new DesktopAcrylicBackdrop(), // Using DesktopAcrylic as thin
|
||||
WindowBackdropType.None => null,
|
||||
_ => new MicaBackdrop() { Kind = Microsoft.UI.Composition.SystemBackdrops.MicaKind.Base }
|
||||
};
|
||||
|
||||
if (windowEx.SystemBackdrop != backdrop)
|
||||
{
|
||||
windowEx.SystemBackdrop = backdrop;
|
||||
|
||||
BackdropChanged?.Invoke(this, backdropType);
|
||||
|
||||
Debug.WriteLine($"Applied backdrop: {backdropType}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to apply backdrop {backdropType}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAccentColorAsync(string hexColor, bool preserveTheme = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hexColor))
|
||||
{
|
||||
// Reset to system accent color
|
||||
hexColor = GetSystemAccentColorHex();
|
||||
}
|
||||
|
||||
if (preserveTheme)
|
||||
{
|
||||
// Just update accent color without changing theme
|
||||
AccentColor = hexColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This might trigger theme changes
|
||||
AccentColor = hexColor;
|
||||
await ApplyCustomThemeAsync(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyThemeUpdate()
|
||||
{
|
||||
if (GetShellRootContent() is not UIElement rootContent) return;
|
||||
|
||||
_ = rootContent.DispatcherQueue.EnqueueAsync(() =>
|
||||
{
|
||||
ElementThemeChanged?.Invoke(this, RootTheme);
|
||||
WeakReferenceMessenger.Default.Send(new ApplicationThemeChanged(_underlyingThemeService.IsUnderlyingThemeDark()));
|
||||
}, Microsoft.UI.Dispatching.DispatcherQueuePriority.High);
|
||||
}
|
||||
|
||||
private void UISettingsColorChanged(UISettings sender, object args)
|
||||
{
|
||||
NotifyThemeUpdate();
|
||||
}
|
||||
|
||||
public void UpdateSystemCaptionButtonColors()
|
||||
{
|
||||
GetShellRootContent().DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (WinoApplication.MainWindow is not WindowEx mainWindow) return;
|
||||
|
||||
var titleBar = mainWindow.AppWindow.TitleBar;
|
||||
if (titleBar == null) return;
|
||||
|
||||
// Determine if current theme is dark
|
||||
bool isDarkTheme = _underlyingThemeService.IsUnderlyingThemeDark();
|
||||
|
||||
// Set button colors based on theme
|
||||
// Normal and inactive backgrounds are transparent, but hover/pressed have subtle backgrounds
|
||||
titleBar.ButtonBackgroundColor = Color.FromArgb(0, 0, 0, 0); // Transparent
|
||||
titleBar.ButtonInactiveBackgroundColor = Color.FromArgb(0, 0, 0, 0); // Transparent
|
||||
|
||||
if (isDarkTheme)
|
||||
{
|
||||
// Dark theme: use light text/icons for better contrast
|
||||
titleBar.ButtonForegroundColor = Color.FromArgb(255, 255, 255, 255); // White
|
||||
titleBar.ButtonInactiveForegroundColor = Color.FromArgb(128, 255, 255, 255); // Semi-transparent white
|
||||
titleBar.ButtonHoverForegroundColor = Color.FromArgb(255, 255, 255, 255); // White
|
||||
titleBar.ButtonPressedForegroundColor = Color.FromArgb(255, 255, 255, 255); // White
|
||||
|
||||
// Subtle hover and pressed backgrounds for dark theme
|
||||
titleBar.ButtonHoverBackgroundColor = Color.FromArgb(20, 255, 255, 255); // Very subtle white overlay
|
||||
titleBar.ButtonPressedBackgroundColor = Color.FromArgb(40, 255, 255, 255); // Slightly more visible white overlay
|
||||
}
|
||||
else
|
||||
{
|
||||
// Light theme: use dark text/icons for better contrast
|
||||
titleBar.ButtonForegroundColor = Color.FromArgb(255, 0, 0, 0); // Black
|
||||
titleBar.ButtonInactiveForegroundColor = Color.FromArgb(128, 0, 0, 0); // Semi-transparent black
|
||||
titleBar.ButtonHoverForegroundColor = Color.FromArgb(255, 0, 0, 0); // Black
|
||||
titleBar.ButtonPressedForegroundColor = Color.FromArgb(255, 0, 0, 0); // Black
|
||||
|
||||
// Subtle hover and pressed backgrounds for light theme
|
||||
titleBar.ButtonHoverBackgroundColor = Color.FromArgb(20, 0, 0, 0); // Very subtle black overlay
|
||||
titleBar.ButtonPressedBackgroundColor = Color.FromArgb(40, 0, 0, 0); // Slightly more visible black overlay
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Updated title bar button colors for {(isDarkTheme ? "dark" : "light")} theme");
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateAccentColor(string hex)
|
||||
{
|
||||
// Change accent color if specified.
|
||||
if (!string.IsNullOrEmpty(hex))
|
||||
{
|
||||
var color = CommunityToolkit.WinUI.Helpers.ColorHelper.ToColor(hex);
|
||||
var brush = new SolidColorBrush(color);
|
||||
|
||||
if (_applicationResourceManager.ContainsResourceKey("SystemAccentColor"))
|
||||
_applicationResourceManager.ReplaceResource("SystemAccentColor", color);
|
||||
|
||||
if (_applicationResourceManager.ContainsResourceKey("NavigationViewSelectionIndicatorForeground"))
|
||||
_applicationResourceManager.ReplaceResource("NavigationViewSelectionIndicatorForeground", brush);
|
||||
|
||||
if (_applicationResourceManager.ContainsResourceKey("SystemControlBackgroundAccentBrush"))
|
||||
_applicationResourceManager.ReplaceResource("SystemControlBackgroundAccentBrush", brush);
|
||||
|
||||
if (_applicationResourceManager.ContainsResourceKey("SystemColorControlAccentBrush"))
|
||||
_applicationResourceManager.ReplaceResource("SystemColorControlAccentBrush", brush);
|
||||
|
||||
RefreshThemeResource();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshThemeResource()
|
||||
{
|
||||
var mainApplicationFrame = GetShellRootContent();
|
||||
|
||||
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)
|
||||
{
|
||||
// If no theme ID is set, don't apply any theme (for backward compatibility)
|
||||
if (currentApplicationThemeId == null)
|
||||
{
|
||||
Debug.WriteLine("No theme ID set, skipping theme application");
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
var customThemes = await GetCurrentCustomThemesAsync();
|
||||
|
||||
controlThemeList.AddRange(customThemes.Select(a => new CustomAppTheme(a)));
|
||||
|
||||
applyingTheme = controlThemeList.Find(a => a.Id == currentApplicationThemeId);
|
||||
|
||||
// If theme ID is not found in available themes, don't apply any theme (backward compatibility)
|
||||
if (applyingTheme == null)
|
||||
{
|
||||
Debug.WriteLine($"Theme with ID {currentApplicationThemeId} not found, skipping theme application");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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.ReadExactlyAsync(bytes);
|
||||
|
||||
var buffer = bytes.AsBuffer();
|
||||
|
||||
await FileIO.WriteBufferAsync(thumbnailFile, buffer);
|
||||
}
|
||||
|
||||
// Save metadata.
|
||||
var metadataFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.json", CreationCollisionOption.ReplaceExisting);
|
||||
|
||||
var serialized = JsonSerializer.Serialize(newTheme, DomainModelsJsonContext.Default.CustomThemeMetadata);
|
||||
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 JsonSerializer.Deserialize(fileContent, DomainModelsJsonContext.Default.CustomThemeMetadata);
|
||||
}
|
||||
|
||||
public string GetSystemAccentColorHex()
|
||||
=> uiSettings.GetColorValue(UIColorType.Accent).ToHex();
|
||||
|
||||
public List<string> GetAvailableAccountColors()
|
||||
{
|
||||
return new List<string>()
|
||||
{
|
||||
"#e74c3c",
|
||||
"#c0392b",
|
||||
"#e53935",
|
||||
"#d81b60",
|
||||
|
||||
// Pinks
|
||||
"#e91e63",
|
||||
"#ec407a",
|
||||
"#ff4081",
|
||||
|
||||
// Purples
|
||||
"#9b59b6",
|
||||
"#8e44ad",
|
||||
"#673ab7",
|
||||
|
||||
// Blues
|
||||
"#3498db",
|
||||
"#2980b9",
|
||||
"#2196f3",
|
||||
"#03a9f4",
|
||||
"#00bcd4",
|
||||
|
||||
// Teals
|
||||
"#009688",
|
||||
"#1abc9c",
|
||||
"#16a085",
|
||||
|
||||
// Greens
|
||||
"#2ecc71",
|
||||
"#27ae60",
|
||||
"#4caf50",
|
||||
"#8bc34a",
|
||||
|
||||
// Yellows & Oranges
|
||||
"#f1c40f",
|
||||
"#f39c12",
|
||||
"#ff9800",
|
||||
"#ff5722",
|
||||
|
||||
// Browns
|
||||
"#795548",
|
||||
"#a0522d",
|
||||
|
||||
// Grays
|
||||
"#9e9e9e",
|
||||
"#607d8b",
|
||||
"#34495e",
|
||||
"#2c3e50",
|
||||
};
|
||||
}
|
||||
|
||||
public List<BackdropTypeWrapper> GetAvailableBackdropTypes()
|
||||
{
|
||||
return new List<BackdropTypeWrapper>
|
||||
{
|
||||
new BackdropTypeWrapper(WindowBackdropType.None, "None"),
|
||||
new BackdropTypeWrapper(WindowBackdropType.Mica, "Mica"),
|
||||
new BackdropTypeWrapper(WindowBackdropType.MicaAlt, "Mica Alt"),
|
||||
new BackdropTypeWrapper(WindowBackdropType.DesktopAcrylic, "Desktop Acrylic"),
|
||||
new BackdropTypeWrapper(WindowBackdropType.AcrylicBase, "Acrylic Base"),
|
||||
new BackdropTypeWrapper(WindowBackdropType.AcrylicThin, "Acrylic Thin")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Serilog;
|
||||
using Windows.Data.Xml.Dom;
|
||||
using Windows.UI.Notifications;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class NotificationBuilder : INotificationBuilder
|
||||
{
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly IFolderService _folderService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IThumbnailService _thumbnailService;
|
||||
|
||||
public NotificationBuilder(IAccountService accountService,
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
IThumbnailService thumbnailService)
|
||||
{
|
||||
_accountService = accountService;
|
||||
_folderService = folderService;
|
||||
_mailService = mailService;
|
||||
_thumbnailService = thumbnailService;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<MailReadStatusChanged>(this, (r, msg) =>
|
||||
{
|
||||
RemoveNotification(msg.UniqueId);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task CreateNotificationsAsync(IEnumerable<MailCopy> downloadedMailItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Filter mails to only include Inbox folder items
|
||||
var inboxMailItems = new List<MailCopy>();
|
||||
|
||||
foreach (var item in downloadedMailItems)
|
||||
{
|
||||
var mailItem = await _mailService.GetSingleMailItemAsync(item.UniqueId);
|
||||
|
||||
//if (mailItem == null || mailItem.AssignedFolder == null)
|
||||
// continue;
|
||||
|
||||
//// Only create notifications for Inbox folder mails
|
||||
//if (mailItem.AssignedFolder.SpecialFolderType != SpecialFolderType.Inbox)
|
||||
// continue;
|
||||
|
||||
//// Skip folders with synchronization disabled
|
||||
//if (!mailItem.AssignedFolder.IsSynchronizationEnabled)
|
||||
// continue;
|
||||
|
||||
//// Skip already read mails
|
||||
//if (mailItem.IsRead)
|
||||
//{
|
||||
// RemoveNotification(mailItem.UniqueId);
|
||||
// continue;
|
||||
//}
|
||||
|
||||
inboxMailItems.Add(mailItem);
|
||||
}
|
||||
|
||||
var mailCount = inboxMailItems.Count;
|
||||
|
||||
if (mailCount == 0)
|
||||
return;
|
||||
|
||||
// If there are more than 3 mails, just display 1 general toast.
|
||||
if (mailCount > 3)
|
||||
{
|
||||
var builder = new ToastContentBuilder();
|
||||
builder.SetToastScenario(ToastScenario.Default);
|
||||
|
||||
builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
|
||||
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
|
||||
|
||||
builder.AddButton(GetDismissButton());
|
||||
builder.AddAudio(new ToastAudio()
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
||||
});
|
||||
|
||||
builder.Show();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var mailItem in inboxMailItems)
|
||||
{
|
||||
await CreateSingleNotificationAsync(mailItem);
|
||||
}
|
||||
|
||||
await UpdateTaskbarIconBadgeAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to create notifications.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateSingleNotificationAsync(MailCopy mailItem)
|
||||
{
|
||||
var builder = new ToastContentBuilder();
|
||||
builder.SetToastScenario(ToastScenario.Default);
|
||||
|
||||
var avatarThumbnail = await _thumbnailService.GetThumbnailAsync(mailItem.FromAddress, awaitLoad: true);
|
||||
if (!string.IsNullOrEmpty(avatarThumbnail))
|
||||
{
|
||||
var tempFile = await Windows.Storage.ApplicationData.Current.TemporaryFolder.CreateFileAsync($"{Guid.NewGuid()}.png", Windows.Storage.CreationCollisionOption.ReplaceExisting);
|
||||
await using (var stream = await tempFile.OpenStreamForWriteAsync())
|
||||
{
|
||||
var bytes = Convert.FromBase64String(avatarThumbnail);
|
||||
await stream.WriteAsync(bytes);
|
||||
}
|
||||
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), hintCrop: ToastGenericAppLogoCrop.Default);
|
||||
}
|
||||
|
||||
// Override system notification timestamp with received date of the mail.
|
||||
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||
|
||||
builder.AddText(mailItem.FromName);
|
||||
builder.AddText(mailItem.Subject);
|
||||
builder.AddText(mailItem.PreviewText);
|
||||
|
||||
builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString());
|
||||
builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate);
|
||||
|
||||
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
||||
builder.AddAudio(new ToastAudio()
|
||||
{
|
||||
Src = new Uri("ms-winsoundevent:Notification.Mail")
|
||||
});
|
||||
|
||||
// Use UniqueId as tag to allow removal
|
||||
builder.Show(toast => toast.Tag = mailItem.UniqueId.ToString());
|
||||
}
|
||||
|
||||
private ToastButton GetDismissButton()
|
||||
=> new ToastButton()
|
||||
.SetDismissActivation()
|
||||
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png"));
|
||||
|
||||
private static ToastButton GetArchiveButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_Archive)
|
||||
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/archive.png"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.Archive)
|
||||
.SetBackgroundActivation();
|
||||
|
||||
private ToastButton GetDeleteButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_Delete)
|
||||
.SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete)
|
||||
.SetBackgroundActivation();
|
||||
|
||||
private static ToastButton GetMarkAsReadButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_MarkAsRead)
|
||||
.SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead)
|
||||
.SetBackgroundActivation();
|
||||
|
||||
public async Task UpdateTaskbarIconBadgeAsync()
|
||||
{
|
||||
int totalUnreadCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
var badgeUpdater = BadgeUpdateManager.CreateBadgeUpdaterForApplication();
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
if (!account.Preferences.IsTaskbarBadgeEnabled) continue;
|
||||
|
||||
var accountInbox = await _folderService.GetSpecialFolderByAccountIdAsync(account.Id, SpecialFolderType.Inbox);
|
||||
|
||||
if (accountInbox == null)
|
||||
continue;
|
||||
|
||||
var inboxUnreadCount = await _folderService.GetFolderNotificationBadgeAsync(accountInbox.Id);
|
||||
|
||||
totalUnreadCount += inboxUnreadCount;
|
||||
}
|
||||
|
||||
if (totalUnreadCount > 0)
|
||||
{
|
||||
// Get the blank badge XML payload for a badge number
|
||||
XmlDocument badgeXml = BadgeUpdateManager.GetTemplateContent(BadgeTemplateType.BadgeNumber);
|
||||
|
||||
// Set the value of the badge in the XML to our number
|
||||
XmlElement badgeElement = badgeXml.SelectSingleNode("/badge") as XmlElement;
|
||||
badgeElement.SetAttribute("value", totalUnreadCount.ToString());
|
||||
|
||||
// Create the badge notification
|
||||
BadgeNotification badge = new BadgeNotification(badgeXml);
|
||||
|
||||
// And update the badge
|
||||
badgeUpdater.Update(badge);
|
||||
}
|
||||
else
|
||||
badgeUpdater.Clear();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error while updating taskbar badge.");
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveNotification(Guid mailUniqueId)
|
||||
{
|
||||
try
|
||||
{
|
||||
ToastNotificationManager.History.Remove(mailUniqueId.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}");
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateAttentionRequiredNotification(MailAccount account)
|
||||
{
|
||||
var builder = new ToastContentBuilder();
|
||||
builder.SetToastScenario(ToastScenario.Default);
|
||||
|
||||
builder.AddText(Translator.Exception_AccountNeedsAttention_Title);
|
||||
builder.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name));
|
||||
|
||||
builder.AddButton(GetDismissButton());
|
||||
|
||||
builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString());
|
||||
builder.AddButton(new ToastButton().SetContent(Translator.Buttons_FixAccount));
|
||||
builder.Show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Reader;
|
||||
using Wino.Core.Domain.Translations;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class PreferencesService(IConfigurationService configurationService) : ObservableObject, IPreferencesService
|
||||
{
|
||||
private readonly IConfigurationService _configurationService = configurationService;
|
||||
|
||||
public event EventHandler<string> PreferenceChanged;
|
||||
|
||||
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
|
||||
PreferenceChanged?.Invoke(this, e.PropertyName);
|
||||
}
|
||||
|
||||
private void SaveProperty(string propertyName, object value) => _configurationService.Set(propertyName, value);
|
||||
|
||||
private void SetPropertyAndSave(string propertyName, object value)
|
||||
{
|
||||
_configurationService.Set(propertyName, value);
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
Debug.WriteLine($"PreferencesService -> {propertyName}:{value?.ToString()}");
|
||||
}
|
||||
|
||||
public MailRenderingOptions GetRenderingOptions()
|
||||
=> new MailRenderingOptions()
|
||||
{
|
||||
LoadImages = RenderImages,
|
||||
LoadStyles = RenderStyles,
|
||||
RenderPlaintextLinks = RenderPlaintextLinks
|
||||
};
|
||||
|
||||
public MailListDisplayMode MailItemDisplayMode
|
||||
{
|
||||
get => _configurationService.Get(nameof(MailItemDisplayMode), MailListDisplayMode.Spacious);
|
||||
set => SetPropertyAndSave(nameof(MailItemDisplayMode), value);
|
||||
}
|
||||
|
||||
public bool IsSemanticZoomEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsSemanticZoomEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsSemanticZoomEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsHardDeleteProtectionEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsHardDeleteProtectionEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsHardDeleteProtectionEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsThreadingEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsThreadingEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsThreadingEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsMailListActionBarEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsMailListActionBarEnabled), false);
|
||||
set => SetPropertyAndSave(nameof(IsMailListActionBarEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsShowActionLabelsEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsShowActionLabelsEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsShowActionLabelsEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsShowSenderPicturesEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsShowSenderPicturesEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsShowSenderPicturesEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsShowPreviewEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsShowPreviewEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsShowPreviewEnabled), value);
|
||||
}
|
||||
|
||||
public bool RenderStyles
|
||||
{
|
||||
get => _configurationService.Get(nameof(RenderStyles), true);
|
||||
set => SetPropertyAndSave(nameof(RenderStyles), value);
|
||||
}
|
||||
|
||||
public bool RenderPlaintextLinks
|
||||
{
|
||||
get => _configurationService.Get(nameof(RenderPlaintextLinks), true);
|
||||
set => SetPropertyAndSave(nameof(RenderPlaintextLinks), value);
|
||||
}
|
||||
|
||||
public bool RenderImages
|
||||
{
|
||||
get => _configurationService.Get(nameof(RenderImages), true);
|
||||
set => SetPropertyAndSave(nameof(RenderImages), value);
|
||||
}
|
||||
|
||||
public bool Prefer24HourTimeFormat
|
||||
{
|
||||
get => _configurationService.Get(nameof(Prefer24HourTimeFormat), false);
|
||||
set => SetPropertyAndSave(nameof(Prefer24HourTimeFormat), value);
|
||||
}
|
||||
|
||||
public MailMarkAsOption MarkAsPreference
|
||||
{
|
||||
get => _configurationService.Get(nameof(MarkAsPreference), MailMarkAsOption.WhenSelected);
|
||||
set => SetPropertyAndSave(nameof(MarkAsPreference), value);
|
||||
}
|
||||
|
||||
public int MarkAsDelay
|
||||
{
|
||||
get => _configurationService.Get(nameof(MarkAsDelay), 5);
|
||||
set => SetPropertyAndSave(nameof(MarkAsDelay), value);
|
||||
}
|
||||
|
||||
public MailOperation RightSwipeOperation
|
||||
{
|
||||
get => _configurationService.Get(nameof(RightSwipeOperation), MailOperation.MarkAsRead);
|
||||
set => SetPropertyAndSave(nameof(RightSwipeOperation), value);
|
||||
}
|
||||
|
||||
public MailOperation LeftSwipeOperation
|
||||
{
|
||||
get => _configurationService.Get(nameof(LeftSwipeOperation), MailOperation.SoftDelete);
|
||||
set => SetPropertyAndSave(nameof(LeftSwipeOperation), value);
|
||||
}
|
||||
|
||||
public bool IsHoverActionsEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsHoverActionsEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsHoverActionsEnabled), value);
|
||||
}
|
||||
|
||||
public MailOperation LeftHoverAction
|
||||
{
|
||||
get => _configurationService.Get(nameof(LeftHoverAction), MailOperation.Archive);
|
||||
set => SetPropertyAndSave(nameof(LeftHoverAction), value);
|
||||
}
|
||||
|
||||
public MailOperation CenterHoverAction
|
||||
{
|
||||
get => _configurationService.Get(nameof(CenterHoverAction), MailOperation.SoftDelete);
|
||||
set => SetPropertyAndSave(nameof(CenterHoverAction), value);
|
||||
}
|
||||
|
||||
public MailOperation RightHoverAction
|
||||
{
|
||||
get => _configurationService.Get(nameof(RightHoverAction), MailOperation.SetFlag);
|
||||
set => SetPropertyAndSave(nameof(RightHoverAction), value);
|
||||
}
|
||||
|
||||
public bool IsLoggingEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsLoggingEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsLoggingEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsMailkitProtocolLoggerEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsMailkitProtocolLoggerEnabled), false);
|
||||
set => SetPropertyAndSave(nameof(IsMailkitProtocolLoggerEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsGravatarEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsGravatarEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsGravatarEnabled), value);
|
||||
}
|
||||
|
||||
public bool IsFaviconEnabled
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsFaviconEnabled), true);
|
||||
set => SetPropertyAndSave(nameof(IsFaviconEnabled), value);
|
||||
}
|
||||
|
||||
public Guid? StartupEntityId
|
||||
{
|
||||
get => _configurationService.Get<Guid?>(nameof(StartupEntityId), null);
|
||||
set => SaveProperty(propertyName: nameof(StartupEntityId), value);
|
||||
}
|
||||
|
||||
public AppLanguage CurrentLanguage
|
||||
{
|
||||
get => _configurationService.Get(nameof(CurrentLanguage), TranslationService.DefaultAppLanguage);
|
||||
set => SaveProperty(propertyName: nameof(CurrentLanguage), value);
|
||||
}
|
||||
|
||||
public string ReaderFont
|
||||
{
|
||||
get => _configurationService.Get(nameof(ReaderFont), "Calibri");
|
||||
set => SaveProperty(propertyName: nameof(ReaderFont), value);
|
||||
}
|
||||
|
||||
public int ReaderFontSize
|
||||
{
|
||||
get => _configurationService.Get(nameof(ReaderFontSize), 14);
|
||||
set => SaveProperty(propertyName: nameof(ReaderFontSize), value);
|
||||
}
|
||||
|
||||
public string ComposerFont
|
||||
{
|
||||
get => _configurationService.Get(nameof(ComposerFont), "Calibri");
|
||||
set => SaveProperty(propertyName: nameof(ComposerFont), value);
|
||||
}
|
||||
|
||||
public int ComposerFontSize
|
||||
{
|
||||
get => _configurationService.Get(nameof(ComposerFontSize), 14);
|
||||
set => SaveProperty(propertyName: nameof(ComposerFontSize), value);
|
||||
}
|
||||
|
||||
public bool IsNavigationPaneOpened
|
||||
{
|
||||
get => _configurationService.Get(nameof(IsNavigationPaneOpened), true);
|
||||
set => SetPropertyAndSave(propertyName: nameof(IsNavigationPaneOpened), value);
|
||||
}
|
||||
|
||||
public bool AutoSelectNextItem
|
||||
{
|
||||
get => _configurationService.Get(nameof(AutoSelectNextItem), true);
|
||||
set => SaveProperty(propertyName: nameof(AutoSelectNextItem), value);
|
||||
}
|
||||
|
||||
public string DiagnosticId
|
||||
{
|
||||
get => _configurationService.Get(nameof(DiagnosticId), Guid.NewGuid().ToString());
|
||||
set => SaveProperty(propertyName: nameof(DiagnosticId), value);
|
||||
}
|
||||
|
||||
public SearchMode DefaultSearchMode
|
||||
{
|
||||
get => _configurationService.Get(nameof(DefaultSearchMode), SearchMode.Local);
|
||||
set => SaveProperty(propertyName: nameof(DefaultSearchMode), value);
|
||||
}
|
||||
|
||||
public DayOfWeek FirstDayOfWeek
|
||||
{
|
||||
get => _configurationService.Get(nameof(FirstDayOfWeek), DayOfWeek.Monday);
|
||||
set => SaveProperty(propertyName: nameof(FirstDayOfWeek), value);
|
||||
}
|
||||
|
||||
public double HourHeight
|
||||
{
|
||||
get => _configurationService.Get(nameof(HourHeight), 60.0);
|
||||
set => SaveProperty(propertyName: nameof(HourHeight), value);
|
||||
}
|
||||
|
||||
public TimeSpan WorkingHourStart
|
||||
{
|
||||
get => _configurationService.Get(nameof(WorkingHourStart), new TimeSpan(8, 0, 0));
|
||||
set => SaveProperty(propertyName: nameof(WorkingHourStart), value);
|
||||
}
|
||||
|
||||
public TimeSpan WorkingHourEnd
|
||||
{
|
||||
get => _configurationService.Get(nameof(WorkingHourEnd), new TimeSpan(17, 0, 0));
|
||||
set => SaveProperty(propertyName: nameof(WorkingHourEnd), value);
|
||||
}
|
||||
|
||||
public DayOfWeek WorkingDayStart
|
||||
{
|
||||
get => _configurationService.Get(nameof(WorkingDayStart), DayOfWeek.Monday);
|
||||
set => SaveProperty(propertyName: nameof(WorkingDayStart), value);
|
||||
}
|
||||
|
||||
public DayOfWeek WorkingDayEnd
|
||||
{
|
||||
get => _configurationService.Get(nameof(WorkingDayEnd), DayOfWeek.Friday);
|
||||
set => SaveProperty(propertyName: nameof(WorkingDayEnd), value);
|
||||
}
|
||||
|
||||
public int EmailSyncIntervalMinutes
|
||||
{
|
||||
get => _configurationService.Get(nameof(EmailSyncIntervalMinutes), 3);
|
||||
set => SetPropertyAndSave(nameof(EmailSyncIntervalMinutes), value);
|
||||
}
|
||||
|
||||
public CalendarSettings GetCurrentCalendarSettings()
|
||||
{
|
||||
var workingDays = GetDaysBetween(WorkingDayStart, WorkingDayEnd);
|
||||
|
||||
return new CalendarSettings(FirstDayOfWeek,
|
||||
workingDays,
|
||||
WorkingHourStart,
|
||||
WorkingHourEnd,
|
||||
HourHeight,
|
||||
Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour,
|
||||
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)));
|
||||
}
|
||||
|
||||
private List<DayOfWeek> GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay)
|
||||
{
|
||||
var daysOfWeek = new List<DayOfWeek>();
|
||||
|
||||
int currentDay = (int)startDay;
|
||||
int endDayInt = (int)endDay;
|
||||
|
||||
// If endDay is before startDay in the week, wrap around
|
||||
if (endDayInt < currentDay)
|
||||
{
|
||||
endDayInt += 7;
|
||||
}
|
||||
|
||||
// Collect days from startDay to endDay
|
||||
while (currentDay <= endDayInt)
|
||||
{
|
||||
daysOfWeek.Add((DayOfWeek)(currentDay % 7));
|
||||
currentDay++;
|
||||
}
|
||||
|
||||
return daysOfWeek;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Printing;
|
||||
using Windows.Data.Pdf;
|
||||
using Windows.Graphics.Printing;
|
||||
using Windows.Graphics.Printing.OptionDetails;
|
||||
using Windows.Storage.Streams;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Printing;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Printer service that uses WinRT APIs to print PDF files.
|
||||
/// Used modified version of the code here:
|
||||
/// https://github.com/microsoft/Win2D-Samples/blob/reunion_master/ExampleGallery/PrintingExample.xaml.cs
|
||||
/// HTML file is saved as PDF to temporary location.
|
||||
/// Then PDF is loaded as PdfDocument and printed using CanvasBitmap for each page.
|
||||
/// </summary>
|
||||
|
||||
public class PrintService : IPrintService
|
||||
{
|
||||
private TaskCompletionSource<PrintingResult> _taskCompletionSource;
|
||||
private CanvasPrintDocument printDocument;
|
||||
private PrintTask printTask;
|
||||
private PdfDocument pdfDocument;
|
||||
|
||||
private List<CanvasBitmap> bitmaps = new();
|
||||
private Vector2 largestBitmap;
|
||||
private Vector2 pageSize;
|
||||
private Vector2 imagePadding = new Vector2(64, 64);
|
||||
private Vector2 cellSize;
|
||||
|
||||
private int bitmapCount;
|
||||
private int columns;
|
||||
private int rows;
|
||||
private int bitmapsPerPage;
|
||||
private int pageCount = -1;
|
||||
|
||||
private PrintInformation _currentPrintInformation;
|
||||
|
||||
public async Task<PrintingResult> PrintPdfFileAsync(string pdfFilePath, string printTitle)
|
||||
{
|
||||
if (_taskCompletionSource != null)
|
||||
{
|
||||
_taskCompletionSource.TrySetResult(PrintingResult.Abandoned);
|
||||
_taskCompletionSource = new TaskCompletionSource<PrintingResult>();
|
||||
}
|
||||
|
||||
// Load the PDF file
|
||||
var file = await Windows.Storage.StorageFile.GetFileFromPathAsync(pdfFilePath);
|
||||
pdfDocument = await PdfDocument.LoadFromFileAsync(file);
|
||||
|
||||
_taskCompletionSource ??= new TaskCompletionSource<PrintingResult>();
|
||||
|
||||
_currentPrintInformation = new PrintInformation(pdfFilePath, printTitle);
|
||||
|
||||
printDocument = new CanvasPrintDocument();
|
||||
printDocument.PrintTaskOptionsChanged += OnDocumentTaskOptionsChanged;
|
||||
printDocument.Preview += OnDocumentPreview;
|
||||
printDocument.Print += OnDocumentPrint;
|
||||
|
||||
var printManager = PrintManager.GetForCurrentView();
|
||||
printManager.PrintTaskRequested += PrintingExample_PrintTaskRequested;
|
||||
|
||||
try
|
||||
{
|
||||
await PrintManager.ShowPrintUIAsync();
|
||||
|
||||
var result = await _taskCompletionSource.Task;
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Dispose everything.
|
||||
UnregisterPrintManager(printManager);
|
||||
ClearBitmaps();
|
||||
UnregisterTask();
|
||||
DisposePDFDocument();
|
||||
|
||||
_taskCompletionSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposePDFDocument()
|
||||
{
|
||||
if (pdfDocument != null)
|
||||
{
|
||||
pdfDocument = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterTask()
|
||||
{
|
||||
if (printTask != null)
|
||||
{
|
||||
printTask.Completed -= TaskCompleted;
|
||||
printTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterPrintManager(PrintManager manager)
|
||||
{
|
||||
manager.PrintTaskRequested -= PrintingExample_PrintTaskRequested;
|
||||
}
|
||||
|
||||
private void ClearBitmaps()
|
||||
{
|
||||
foreach (var bitmap in bitmaps)
|
||||
{
|
||||
bitmap.Dispose();
|
||||
}
|
||||
|
||||
bitmaps.Clear();
|
||||
}
|
||||
|
||||
private void PrintingExample_PrintTaskRequested(PrintManager sender, PrintTaskRequestedEventArgs args)
|
||||
{
|
||||
if (_currentPrintInformation == null) return;
|
||||
|
||||
printTask = args.Request.CreatePrintTask(_currentPrintInformation.PDFTitle, (createPrintTaskArgs) =>
|
||||
{
|
||||
createPrintTaskArgs.SetSource(printDocument);
|
||||
});
|
||||
|
||||
printTask.Completed += TaskCompleted;
|
||||
}
|
||||
|
||||
private void TaskCompleted(PrintTask sender, PrintTaskCompletedEventArgs args)
|
||||
=> _taskCompletionSource?.TrySetResult((PrintingResult)args.Completion);
|
||||
|
||||
private async void OnDocumentTaskOptionsChanged(CanvasPrintDocument sender, CanvasPrintTaskOptionsChangedEventArgs args)
|
||||
{
|
||||
var deferral = args.GetDeferral();
|
||||
|
||||
try
|
||||
{
|
||||
await LoadPDFPageBitmapsAsync(sender);
|
||||
|
||||
var pageDesc = args.PrintTaskOptions.GetPageDescription(1);
|
||||
var newPageSize = pageDesc.PageSize.ToVector2();
|
||||
|
||||
if (pageSize == newPageSize && pageCount != -1)
|
||||
{
|
||||
// We've already figured out the pages and the page size hasn't changed, so there's nothing left for us to do here.
|
||||
return;
|
||||
}
|
||||
|
||||
pageSize = newPageSize;
|
||||
sender.InvalidatePreview();
|
||||
|
||||
// Figure out the bitmap index at the top of the current preview page. We'll request that the preview defaults to showing
|
||||
// the page that still has this bitmap on it in the new layout.
|
||||
int indexOnCurrentPage = 0;
|
||||
if (pageCount != -1)
|
||||
{
|
||||
indexOnCurrentPage = (int)(args.CurrentPreviewPageNumber - 1) * bitmapsPerPage;
|
||||
}
|
||||
|
||||
// Calculate the new layout
|
||||
var printablePageSize = pageSize * 0.9f;
|
||||
|
||||
cellSize = largestBitmap + imagePadding;
|
||||
|
||||
var cellsPerPage = printablePageSize / cellSize;
|
||||
|
||||
columns = Math.Max(1, (int)Math.Floor(cellsPerPage.X));
|
||||
rows = Math.Max(1, (int)Math.Floor(cellsPerPage.Y));
|
||||
|
||||
bitmapsPerPage = columns * rows;
|
||||
|
||||
// Calculate the page count
|
||||
bitmapCount = bitmaps.Count;
|
||||
pageCount = (int)Math.Ceiling(bitmapCount / (double)bitmapsPerPage);
|
||||
sender.SetPageCount((uint)pageCount);
|
||||
|
||||
// Set the preview page to the one that has the item that was currently displayed in the last preview
|
||||
args.NewPreviewPageNumber = (uint)(indexOnCurrentPage / bitmapsPerPage) + 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
deferral.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task LoadPDFPageBitmapsAsync(CanvasPrintDocument sender)
|
||||
{
|
||||
ClearBitmaps();
|
||||
|
||||
bitmaps ??= new List<CanvasBitmap>();
|
||||
|
||||
for (int i = 0; i < pdfDocument.PageCount; i++)
|
||||
{
|
||||
var page = pdfDocument.GetPage((uint)i);
|
||||
var stream = new InMemoryRandomAccessStream();
|
||||
await page.RenderToStreamAsync(stream);
|
||||
var bitmap = await CanvasBitmap.LoadAsync(sender, stream);
|
||||
bitmaps.Add(bitmap);
|
||||
}
|
||||
|
||||
largestBitmap = Vector2.Zero;
|
||||
|
||||
foreach (var bitmap in bitmaps)
|
||||
{
|
||||
largestBitmap.X = Math.Max(largestBitmap.X, (float)bitmap.Size.Width);
|
||||
largestBitmap.Y = Math.Max(largestBitmap.Y, (float)bitmap.Size.Height);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnDocumentPreview(CanvasPrintDocument sender, CanvasPreviewEventArgs args)
|
||||
{
|
||||
var ds = args.DrawingSession;
|
||||
var pageNumber = args.PageNumber;
|
||||
|
||||
DrawPdfPage(sender, ds, pageNumber);
|
||||
}
|
||||
|
||||
private void OnDocumentPrint(CanvasPrintDocument sender, CanvasPrintEventArgs args)
|
||||
{
|
||||
var detailedOptions = PrintTaskOptionDetails.GetFromPrintTaskOptions(args.PrintTaskOptions);
|
||||
|
||||
int pageCountToPrint = (int)pdfDocument.PageCount;
|
||||
|
||||
for (uint i = 1; i <= pageCountToPrint; ++i)
|
||||
{
|
||||
using var ds = args.CreateDrawingSession();
|
||||
var imageableRect = args.PrintTaskOptions.GetPageDescription(i).ImageableRect;
|
||||
|
||||
DrawPdfPage(sender, ds, i);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPdfPage(CanvasPrintDocument sender, CanvasDrawingSession ds, uint pageNumber)
|
||||
{
|
||||
if (bitmaps?.Count == 0) return;
|
||||
|
||||
var cellAcross = new Vector2(cellSize.X, 0);
|
||||
var cellDown = new Vector2(0, cellSize.Y);
|
||||
|
||||
var totalSize = cellAcross * columns + cellDown * rows;
|
||||
Vector2 topLeft = (pageSize - totalSize) / 2;
|
||||
|
||||
int bitmapIndex = ((int)pageNumber - 1) * bitmapsPerPage;
|
||||
|
||||
for (int row = 0; row < rows; ++row)
|
||||
{
|
||||
for (int column = 0; column < columns; ++column)
|
||||
{
|
||||
var cellTopLeft = topLeft + cellAcross * column + cellDown * row;
|
||||
var bitmapInfo = bitmaps[bitmapIndex % bitmaps.Count];
|
||||
var bitmapPos = cellTopLeft + (cellSize - bitmapInfo.Size.ToVector2()) / 2;
|
||||
|
||||
ds.DrawImage(bitmapInfo, bitmapPos);
|
||||
|
||||
bitmapIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using Windows.ApplicationModel;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class StartupBehaviorService : IStartupBehaviorService
|
||||
{
|
||||
private const string WinoServerTaskId = "WinoStartupId";
|
||||
|
||||
public async Task<StartupBehaviorResult> ToggleStartupBehavior(bool isEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = await StartupTask.GetAsync(WinoServerTaskId);
|
||||
|
||||
if (isEnabled)
|
||||
{
|
||||
await task.RequestEnableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
task.Disable();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Log.Error("Error toggling startup behavior");
|
||||
|
||||
}
|
||||
|
||||
return await GetCurrentStartupBehaviorAsync();
|
||||
}
|
||||
|
||||
public async Task<StartupBehaviorResult> GetCurrentStartupBehaviorAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = await StartupTask.GetAsync(WinoServerTaskId);
|
||||
|
||||
return task.State.AsStartupBehaviorResult();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error getting startup behavior");
|
||||
|
||||
return StartupBehaviorResult.Fatal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Mail.WinUI;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class StatePersistenceService : ObservableObject, IStatePersistanceService
|
||||
{
|
||||
public event EventHandler<string> StatePropertyChanged;
|
||||
|
||||
private const string OpenPaneLengthKey = nameof(OpenPaneLengthKey);
|
||||
private const string MailListPaneLengthKey = nameof(MailListPaneLengthKey);
|
||||
|
||||
private readonly IConfigurationService _configurationService;
|
||||
|
||||
public StatePersistenceService(IConfigurationService configurationService)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
|
||||
_openPaneLength = _configurationService.Get(OpenPaneLengthKey, 320d);
|
||||
_mailListPaneLength = _configurationService.Get(MailListPaneLengthKey, 420d);
|
||||
_calendarDisplayType = _configurationService.Get(nameof(CalendarDisplayType), CalendarDisplayType.Week);
|
||||
_dayDisplayCount = _configurationService.Get(nameof(DayDisplayCount), 1);
|
||||
|
||||
PropertyChanged += ServicePropertyChanged;
|
||||
}
|
||||
|
||||
private void ServicePropertyChanged(object sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e.PropertyName);
|
||||
|
||||
public bool IsBackButtonVisible => IsReadingMail && IsReaderNarrowed;
|
||||
|
||||
private bool isReadingMail;
|
||||
|
||||
public bool IsReadingMail
|
||||
{
|
||||
get => isReadingMail;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref isReadingMail, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsBackButtonVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool shouldShiftMailRenderingDesign;
|
||||
|
||||
public bool ShouldShiftMailRenderingDesign
|
||||
{
|
||||
get { return shouldShiftMailRenderingDesign; }
|
||||
set { shouldShiftMailRenderingDesign = value; }
|
||||
}
|
||||
|
||||
private bool isReaderNarrowed;
|
||||
|
||||
public bool IsReaderNarrowed
|
||||
{
|
||||
get => isReaderNarrowed;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref isReaderNarrowed, value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsBackButtonVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string coreWindowTitle;
|
||||
|
||||
public string CoreWindowTitle
|
||||
{
|
||||
get => coreWindowTitle;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref coreWindowTitle, value))
|
||||
{
|
||||
UpdateAppCoreWindowTitle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double _openPaneLength;
|
||||
public double OpenPaneLength
|
||||
{
|
||||
get => _openPaneLength;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _openPaneLength, value))
|
||||
{
|
||||
_configurationService.Set(OpenPaneLengthKey, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double _mailListPaneLength;
|
||||
public double MailListPaneLength
|
||||
{
|
||||
get => _mailListPaneLength;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _mailListPaneLength, value))
|
||||
{
|
||||
_configurationService.Set(MailListPaneLengthKey, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CalendarDisplayType _calendarDisplayType;
|
||||
public CalendarDisplayType CalendarDisplayType
|
||||
{
|
||||
get => _calendarDisplayType;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _calendarDisplayType, value))
|
||||
{
|
||||
_configurationService.Set(nameof(CalendarDisplayType), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int _dayDisplayCount;
|
||||
public int DayDisplayCount
|
||||
{
|
||||
get => _dayDisplayCount;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _dayDisplayCount, value))
|
||||
{
|
||||
_configurationService.Set(nameof(DayDisplayCount), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAppCoreWindowTitle() => WinoApplication.MainWindow.Title = CoreWindowTitle;
|
||||
}
|
||||
@@ -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;
|
||||
using WinoStorePurchaseResult = Wino.Core.Domain.Enums.StorePurchaseResult;
|
||||
|
||||
namespace Wino.Mail.WinUI.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<WinoStorePurchaseResult> PurchaseAsync(StoreProductType productType)
|
||||
{
|
||||
if (await HasProductAsync(productType))
|
||||
return WinoStorePurchaseResult.AlreadyPurchased;
|
||||
else
|
||||
{
|
||||
var productKey = skuIds[productType];
|
||||
|
||||
var result = await CurrentContext.RequestPurchaseAsync(productKey);
|
||||
|
||||
switch (result.Status)
|
||||
{
|
||||
case StorePurchaseStatus.Succeeded:
|
||||
return WinoStorePurchaseResult.Succeeded;
|
||||
case StorePurchaseStatus.AlreadyPurchased:
|
||||
return WinoStorePurchaseResult.AlreadyPurchased;
|
||||
default:
|
||||
return WinoStorePurchaseResult.NotPurchased;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
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.Mail.WinUI.Services;
|
||||
|
||||
public class StoreRatingService : IStoreRatingService
|
||||
{
|
||||
private const string RatedStorageKey = nameof(RatedStorageKey);
|
||||
private const string LatestAskedKey = nameof(LatestAskedKey);
|
||||
|
||||
private readonly IConfigurationService _configurationService;
|
||||
private readonly IMailDialogService _dialogService;
|
||||
|
||||
public StoreRatingService(IConfigurationService configurationService, IMailDialogService dialogService)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
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 isRateWinoApproved = await _dialogService.ShowWinoCustomMessageDialogAsync(Translator.StoreRatingDialog_Title,
|
||||
Translator.StoreRatingDialog_MessageFirstLine,
|
||||
Translator.Buttons_RateWino,
|
||||
Wino.Core.Domain.Enums.WinoCustomMessageDialogIcon.Question,
|
||||
Translator.Buttons_No,
|
||||
RatedStorageKey);
|
||||
|
||||
if (isRateWinoApproved)
|
||||
{
|
||||
// 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, Wino.Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
else
|
||||
_dialogService.InfoBarMessage(Translator.Info_ReviewSuccessTitle, Translator.Info_ReviewNewMessage, Wino.Core.Domain.Enums.InfoBarMessageType.Success);
|
||||
|
||||
_configurationService.Set(RatedStorageKey, true);
|
||||
break;
|
||||
case StoreRateAndReviewStatus.CanceledByUser:
|
||||
break;
|
||||
|
||||
case StoreRateAndReviewStatus.NetworkError:
|
||||
_dialogService.InfoBarMessage(Translator.Info_ReviewNetworkErrorTitle, Translator.Info_ReviewNetworkErrorMessage, Wino.Core.Domain.Enums.InfoBarMessageType.Warning);
|
||||
break;
|
||||
default:
|
||||
_dialogService.InfoBarMessage(Translator.Info_ReviewUnknownErrorTitle, string.Format(Translator.Info_ReviewUnknownErrorMessage, result.ExtendedError.Message), Wino.Core.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) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Mail;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Gravatar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Messaging.UI;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.Mail.WinUI.Services;
|
||||
|
||||
public class ThumbnailService(IPreferencesService preferencesService, IDatabaseService databaseService) : IThumbnailService
|
||||
{
|
||||
private readonly IPreferencesService _preferencesService = preferencesService;
|
||||
private readonly IDatabaseService _databaseService = databaseService;
|
||||
private static readonly HttpClient _httpClient = new();
|
||||
private bool _isInitialized = false;
|
||||
|
||||
private ConcurrentDictionary<string, (string graviton, string favicon)> _cache;
|
||||
private readonly ConcurrentDictionary<string, Task> _requests = [];
|
||||
|
||||
private static readonly List<string> _excludedFaviconDomains = [
|
||||
"gmail.com",
|
||||
"outlook.com",
|
||||
"hotmail.com",
|
||||
"live.com",
|
||||
"yahoo.com",
|
||||
"icloud.com",
|
||||
"aol.com",
|
||||
"protonmail.com",
|
||||
"zoho.com",
|
||||
"mail.com",
|
||||
"gmx.com",
|
||||
"yandex.com",
|
||||
"yandex.ru",
|
||||
"tutanota.com",
|
||||
"mail.ru",
|
||||
"rediffmail.com"
|
||||
];
|
||||
|
||||
public async ValueTask<string> GetThumbnailAsync(string email, bool awaitLoad = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
return null;
|
||||
|
||||
if (!_preferencesService.IsShowSenderPicturesEnabled)
|
||||
return null;
|
||||
|
||||
if (!_isInitialized)
|
||||
{
|
||||
var thumbnailsList = await _databaseService.Connection.Table<Thumbnail>().ToListAsync();
|
||||
|
||||
_cache = new ConcurrentDictionary<string, (string graviton, string favicon)>(
|
||||
thumbnailsList.ToDictionary(x => x.Domain, x => (x.Gravatar, x.Favicon)));
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
var sanitizedEmail = email.Trim().ToLowerInvariant();
|
||||
|
||||
var (gravatar, favicon) = await GetThumbnailInternal(sanitizedEmail, awaitLoad);
|
||||
|
||||
if (_preferencesService.IsGravatarEnabled && !string.IsNullOrEmpty(gravatar))
|
||||
{
|
||||
return gravatar;
|
||||
}
|
||||
|
||||
if (_preferencesService.IsFaviconEnabled && !string.IsNullOrEmpty(favicon))
|
||||
{
|
||||
return favicon;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task ClearCache()
|
||||
{
|
||||
_cache?.Clear();
|
||||
_requests.Clear();
|
||||
await _databaseService.Connection.DeleteAllAsync<Thumbnail>();
|
||||
}
|
||||
|
||||
private async ValueTask<(string gravatar, string favicon)> GetThumbnailInternal(string email, bool awaitLoad)
|
||||
{
|
||||
if (_cache.TryGetValue(email, out var cached))
|
||||
return cached;
|
||||
|
||||
// No network available, skip fetching Gravatar
|
||||
// Do not cache it, since network can be available later
|
||||
//bool isInternetAvailable = GetIsInternetAvailable();
|
||||
|
||||
//if (!isInternetAvailable)
|
||||
// return default;
|
||||
|
||||
if (!_requests.TryGetValue(email, out var request))
|
||||
{
|
||||
request = Task.Run(() => RequestNewThumbnail(email));
|
||||
_requests[email] = request;
|
||||
}
|
||||
|
||||
if (awaitLoad)
|
||||
{
|
||||
await request;
|
||||
_cache.TryGetValue(email, out cached);
|
||||
return cached;
|
||||
}
|
||||
|
||||
return default;
|
||||
|
||||
//static bool GetIsInternetAvailable()
|
||||
//{
|
||||
// var connection = NetworkInformation.GetInternetConnectionProfile();
|
||||
// return connection != null && connection.GetNetworkConnectivityLevel() == NetworkConnectivityLevel.InternetAccess;
|
||||
//}
|
||||
}
|
||||
|
||||
private async Task RequestNewThumbnail(string email)
|
||||
{
|
||||
var gravatarBase64 = await GetGravatarBase64(email);
|
||||
var faviconBase64 = await GetFaviconBase64(email);
|
||||
|
||||
await _databaseService.Connection.InsertOrReplaceAsync(new Thumbnail
|
||||
{
|
||||
Domain = email,
|
||||
Gravatar = gravatarBase64,
|
||||
Favicon = faviconBase64,
|
||||
LastUpdated = DateTime.UtcNow
|
||||
}, typeof(Thumbnail));
|
||||
|
||||
_ = _cache.TryAdd(email, (gravatarBase64, faviconBase64));
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new ThumbnailAdded(email));
|
||||
}
|
||||
|
||||
private static async Task<string> GetGravatarBase64(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var gravatarUrl = GravatarHelper.GetAvatarUrl(
|
||||
email,
|
||||
size: 128,
|
||||
defaultValue: GravatarAvatarDefault.Blank,
|
||||
withFileExtension: false).ToString().Replace("d=blank", "d=404");
|
||||
var response = await _httpClient.GetAsync(gravatarUrl);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<string> GetFaviconBase64(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var host = GetHost(email);
|
||||
|
||||
if (string.IsNullOrEmpty(host))
|
||||
return null;
|
||||
|
||||
// Do not fetch favicon for specific default domains of major platforms
|
||||
if (_excludedFaviconDomains.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
var primaryDomain = string.Join('.', host.Split('.')[^2..]);
|
||||
|
||||
var googleFaviconUrl = $"https://www.google.com/s2/favicons?sz=128&domain_url={primaryDomain}";
|
||||
var response = await _httpClient.GetAsync(googleFaviconUrl);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var bytes = response.Content.ReadAsByteArrayAsync().Result;
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetHost(string email)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(email) && email.Contains('@'))
|
||||
{
|
||||
var split = email.Split('@');
|
||||
if (split.Length >= 2 && !string.IsNullOrEmpty(split[1]))
|
||||
{
|
||||
try { return new MailAddress(email).Host; } catch { }
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Windows.UI.ViewManagement;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Mail.WinUI.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;
|
||||
}
|
||||
|
||||
public bool IsUnderlyingThemeDark()
|
||||
{
|
||||
var currentTheme = _configurationService.Get(SelectedAppThemeKey, ApplicationElementTheme.Default);
|
||||
|
||||
if (currentTheme == ApplicationElementTheme.Default)
|
||||
return uiSettings.GetColorValue(UIColorType.Background).ToString() == "#FF000000";
|
||||
else
|
||||
return currentTheme == ApplicationElementTheme.Dark;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user