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