From 7a69c87b56140ee95f0648552ce18a60f3561c4e Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Mon, 13 May 2024 23:56:21 +0300 Subject: [PATCH] Use a 3-way theme switcher instead of a 2-way switcher (#1233) --- .../DiscordChatExporter.Core.csproj | 2 +- DiscordChatExporter.Gui/App.axaml | 9 +- DiscordChatExporter.Gui/App.axaml.cs | 104 +++++++++++------- .../LocaleToDisplayNameStringConverter.cs | 2 +- .../DiscordChatExporter.Gui.csproj | 2 +- .../Framework/ThemeVariant.cs | 8 ++ .../Services/SettingsService.cs | 19 +--- .../Utils/Extensions/AvaloniaExtensions.cs | 3 +- .../ViewModels/Dialogs/SettingsViewModel.cs | 15 +-- .../ViewModels/MainViewModel.cs | 12 -- .../Views/Dialogs/SettingsView.axaml | 26 ++--- .../Views/Dialogs/SettingsView.axaml.cs | 20 +--- 12 files changed, 106 insertions(+), 116 deletions(-) create mode 100644 DiscordChatExporter.Gui/Framework/ThemeVariant.cs diff --git a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj index eaa04cb6..c0a434c1 100644 --- a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj +++ b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj @@ -5,7 +5,7 @@ - + diff --git a/DiscordChatExporter.Gui/App.axaml b/DiscordChatExporter.Gui/App.axaml index 52f0d270..66e43889 100644 --- a/DiscordChatExporter.Gui/App.axaml +++ b/DiscordChatExporter.Gui/App.axaml @@ -7,13 +7,18 @@ xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles" xmlns:materialControls="clr-namespace:Material.Styles.Controls;assembly=Material.Styles" xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:materialStyles="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"> + xmlns:materialStyles="clr-namespace:Material.Styles.Themes;assembly=Material.Styles" + ActualThemeVariantChanged="Application_OnActualThemeVariantChanged"> - + + diff --git a/DiscordChatExporter.Gui/App.axaml.cs b/DiscordChatExporter.Gui/App.axaml.cs index 5032d8db..05eb9c05 100644 --- a/DiscordChatExporter.Gui/App.axaml.cs +++ b/DiscordChatExporter.Gui/App.axaml.cs @@ -7,6 +7,8 @@ using Avalonia.Media; using Avalonia.Platform; using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Services; +using DiscordChatExporter.Gui.Utils; +using DiscordChatExporter.Gui.Utils.Extensions; using DiscordChatExporter.Gui.ViewModels; using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Dialogs; @@ -16,11 +18,14 @@ using Microsoft.Extensions.DependencyInjection; namespace DiscordChatExporter.Gui; -public partial class App : Application, IDisposable +public class App : Application, IDisposable { private readonly ServiceProvider _services; + private readonly SettingsService _settingsService; private readonly MainViewModel _mainViewModel; + private readonly DisposableCollector _eventRoot = new(); + public App() { var services = new ServiceCollection(); @@ -43,17 +48,62 @@ public partial class App : Application, IDisposable services.AddTransient(); _services = services.BuildServiceProvider(true); + _settingsService = _services.GetRequiredService(); _mainViewModel = _services.GetRequiredService().CreateMainViewModel(); + + // Re-initialize the theme when the user changes it + _eventRoot.Add( + _settingsService.WatchProperty( + o => o.Theme, + () => + { + RequestedThemeVariant = _settingsService.Theme switch + { + ThemeVariant.System => Avalonia.Styling.ThemeVariant.Default, + ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light, + ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark, + _ + => throw new InvalidOperationException( + $"Unknown theme '{_settingsService.Theme}'." + ) + }; + + InitializeTheme(); + }, + false + ) + ); } public override void Initialize() { + base.Initialize(); + // Increase maximum concurrent connections ServicePointManager.DefaultConnectionLimit = 20; AvaloniaXamlLoader.Load(this); } + private void InitializeTheme() + { + var actualTheme = RequestedThemeVariant?.Key switch + { + "Light" => PlatformThemeVariant.Light, + "Dark" => PlatformThemeVariant.Dark, + _ => PlatformSettings?.GetColorValues().ThemeVariant + }; + + this.LocateMaterialTheme().CurrentTheme = actualTheme switch + { + PlatformThemeVariant.Light + => Theme.Create(Theme.Light, Color.Parse("#343838"), Color.Parse("#F9A825")), + PlatformThemeVariant.Dark + => Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825")), + _ => throw new InvalidOperationException($"Unknown theme '{actualTheme}'.") + }; + } + public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) @@ -61,50 +111,20 @@ public partial class App : Application, IDisposable base.OnFrameworkInitializationCompleted(); - // Set custom theme colors - SetDefaultTheme(); + // Set up custom theme colors + InitializeTheme(); + + // Load settings + _settingsService.Load(); } - public void Dispose() => _services.Dispose(); -} + private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) => + // Re-initialize the theme when the system theme changes + InitializeTheme(); -public partial class App -{ - public static void SetLightTheme() + public void Dispose() { - if (Current is null) - return; - - Current.LocateMaterialTheme().CurrentTheme = Theme.Create( - Theme.Light, - Color.Parse("#343838"), - Color.Parse("#F9A825") - ); - } - - public static void SetDarkTheme() - { - if (Current is null) - return; - - Current.LocateMaterialTheme().CurrentTheme = Theme.Create( - Theme.Dark, - Color.Parse("#E8E8E8"), - Color.Parse("#F9A825") - ); - } - - public static void SetDefaultTheme() - { - if (Current is null) - return; - - var isDarkModeEnabledByDefault = - Current.PlatformSettings?.GetColorValues().ThemeVariant == PlatformThemeVariant.Dark; - - if (isDarkModeEnabledByDefault) - SetDarkTheme(); - else - SetLightTheme(); + _eventRoot.Dispose(); + _services.Dispose(); } } diff --git a/DiscordChatExporter.Gui/Converters/LocaleToDisplayNameStringConverter.cs b/DiscordChatExporter.Gui/Converters/LocaleToDisplayNameStringConverter.cs index 5ec65d9d..af1c2131 100644 --- a/DiscordChatExporter.Gui/Converters/LocaleToDisplayNameStringConverter.cs +++ b/DiscordChatExporter.Gui/Converters/LocaleToDisplayNameStringConverter.cs @@ -11,7 +11,7 @@ public class LocaleToDisplayNameStringConverter : IValueConverter public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is string locale && !string.IsNullOrWhiteSpace(locale) ? CultureInfo.GetCultureInfo(locale).DisplayName - : "System default"; + : "System"; public object ConvertBack( object? value, diff --git a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj index 140d150b..5574454d 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -22,7 +22,7 @@ - + diff --git a/DiscordChatExporter.Gui/Framework/ThemeVariant.cs b/DiscordChatExporter.Gui/Framework/ThemeVariant.cs new file mode 100644 index 00000000..e89751d8 --- /dev/null +++ b/DiscordChatExporter.Gui/Framework/ThemeVariant.cs @@ -0,0 +1,8 @@ +namespace DiscordChatExporter.Gui.Framework; + +public enum ThemeVariant +{ + System, + Light, + Dark +} diff --git a/DiscordChatExporter.Gui/Services/SettingsService.cs b/DiscordChatExporter.Gui/Services/SettingsService.cs index a7c27c36..04e40a45 100644 --- a/DiscordChatExporter.Gui/Services/SettingsService.cs +++ b/DiscordChatExporter.Gui/Services/SettingsService.cs @@ -1,12 +1,10 @@ using System; using System.IO; -using Avalonia; -using Avalonia.Platform; using Cogwheel; using CommunityToolkit.Mvvm.ComponentModel; using DiscordChatExporter.Core.Exporting; +using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Models; -using Microsoft.Win32; namespace DiscordChatExporter.Gui.Services; @@ -18,10 +16,10 @@ public partial class SettingsService() private bool _isUkraineSupportMessageEnabled = true; [ObservableProperty] - private bool _isAutoUpdateEnabled = true; + private ThemeVariant _theme; [ObservableProperty] - private bool _isDarkModeEnabled; + private bool _isAutoUpdateEnabled = true; [ObservableProperty] private bool _isTokenPersisted = true; @@ -62,17 +60,6 @@ public partial class SettingsService() [ObservableProperty] private string? _lastAssetsDirPath; - public override void Reset() - { - base.Reset(); - - // Reset the dark mode setting separately because its default value is evaluated dynamically - // and cannot be set by the field initializer. - IsDarkModeEnabled = - Application.Current?.PlatformSettings?.GetColorValues().ThemeVariant - == PlatformThemeVariant.Dark; - } - public override void Save() { // Clear the token if it's not supposed to be persisted diff --git a/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs b/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs index 15df033c..9d339a15 100644 --- a/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs +++ b/DiscordChatExporter.Gui/Utils/Extensions/AvaloniaExtensions.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.VisualTree; diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs index 712da14c..4193e6a7 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Models; @@ -23,18 +22,20 @@ public class SettingsViewModel : DialogViewModelBase _eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged)); } + public IReadOnlyList AvailableThemes { get; } = Enum.GetValues(); + + public ThemeVariant Theme + { + get => _settingsService.Theme; + set => _settingsService.Theme = value; + } + public bool IsAutoUpdateEnabled { get => _settingsService.IsAutoUpdateEnabled; set => _settingsService.IsAutoUpdateEnabled = value; } - public bool IsDarkModeEnabled - { - get => _settingsService.IsDarkModeEnabled; - set => _settingsService.IsDarkModeEnabled = value; - } - public bool IsTokenPersisted { get => _settingsService.IsTokenPersisted; diff --git a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs index c6e85ebd..deced764 100644 --- a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs @@ -79,18 +79,6 @@ public partial class MainViewModel( [RelayCommand] private async Task InitializeAsync() { - // Reset settings (needed to resolve the default dark mode setting) - settingsService.Reset(); - - // Load settings - settingsService.Load(); - - // Set the correct theme - if (settingsService.IsDarkModeEnabled) - App.SetDarkTheme(); - else - App.SetLightTheme(); - await ShowUkraineSupportMessageAsync(); await CheckForUpdatesAsync(); } diff --git a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml index 8cf0e9ad..31b06dbb 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml @@ -24,6 +24,19 @@ BorderThickness="0,1"> + + + + + + - - - - - - { public SettingsView() => InitializeComponent(); - - private void DarkModeToggleSwitch_OnIsCheckedChanged(object? sender, RoutedEventArgs args) - { - if (DarkModeToggleSwitch.IsChecked is true) - { - App.SetDarkTheme(); - } - else if (DarkModeToggleSwitch.IsChecked is false) - { - App.SetLightTheme(); - } - else - { - App.SetDefaultTheme(); - } - } }