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