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 | 102 +++++++++++------- .../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, 105 insertions(+), 115 deletions(-) create mode 100644 DiscordChatExporter.Gui/Framework/ThemeVariant.cs diff --git a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj index eaa04cb..c0a434c 100644 --- a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj +++ b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj @@ -5,7 +5,7 @@ <PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" /> <PackageReference Include="Gress" Version="2.1.1" /> <PackageReference Include="JsonExtensions" Version="1.2.0" /> - <PackageReference Include="Polly" Version="8.3.1" /> + <PackageReference Include="Polly" Version="8.4.0" /> <PackageReference Include="RazorBlade" Version="0.6.0" /> <PackageReference Include="Superpower" Version="3.0.0" /> <PackageReference Include="WebMarkupMin.Core" Version="2.16.0" /> diff --git a/DiscordChatExporter.Gui/App.axaml b/DiscordChatExporter.Gui/App.axaml index 52f0d27..66e4388 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"> <Application.DataTemplates> <framework:ViewManager /> </Application.DataTemplates> <Application.Styles> - <materialStyles:MaterialTheme /> + <!-- This theme is used as a stub to pre-load default resources, the actual colors are set through code --> + <materialStyles:MaterialTheme + BaseTheme="Light" + PrimaryColor="Grey" + SecondaryColor="DeepOrange" /> <materialIcons:MaterialIconStyles /> <dialogHostAvalonia:DialogHostStyles /> diff --git a/DiscordChatExporter.Gui/App.axaml.cs b/DiscordChatExporter.Gui/App.axaml.cs index 5032d8d..05eb9c0 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<SettingsViewModel>(); _services = services.BuildServiceProvider(true); + _settingsService = _services.GetRequiredService<SettingsService>(); _mainViewModel = _services.GetRequiredService<ViewModelManager>().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<MaterialThemeBase>().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(); - } - - public void Dispose() => _services.Dispose(); -} - -public partial class App -{ - public static void SetLightTheme() - { - if (Current is null) - return; + // Set up custom theme colors + InitializeTheme(); - Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create( - Theme.Light, - Color.Parse("#343838"), - Color.Parse("#F9A825") - ); + // Load settings + _settingsService.Load(); } - public static void SetDarkTheme() - { - if (Current is null) - return; - - Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create( - Theme.Dark, - Color.Parse("#E8E8E8"), - Color.Parse("#F9A825") - ); - } + private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) => + // Re-initialize the theme when the system theme changes + InitializeTheme(); - public static void SetDefaultTheme() + public void Dispose() { - 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 5ec65d9..af1c213 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 140d150..5574454 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -22,7 +22,7 @@ <PackageReference Include="DialogHost.Avalonia" Version="0.7.7" /> <PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.4" PrivateAssets="all" /> <PackageReference Include="Gress" Version="2.1.1" /> - <PackageReference Include="Material.Avalonia" Version="3.5.0" /> + <PackageReference Include="Material.Avalonia" Version="3.6.0" /> <PackageReference Include="Material.Icons.Avalonia" Version="2.1.9" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Onova" Version="2.6.11" /> diff --git a/DiscordChatExporter.Gui/Framework/ThemeVariant.cs b/DiscordChatExporter.Gui/Framework/ThemeVariant.cs new file mode 100644 index 0000000..e89751d --- /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 a7c27c3..04e40a4 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 15df033..9d339a1 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 712da14..4193e6a 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,16 +22,18 @@ public class SettingsViewModel : DialogViewModelBase _eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged)); } - public bool IsAutoUpdateEnabled + public IReadOnlyList<ThemeVariant> AvailableThemes { get; } = Enum.GetValues<ThemeVariant>(); + + public ThemeVariant Theme { - get => _settingsService.IsAutoUpdateEnabled; - set => _settingsService.IsAutoUpdateEnabled = value; + get => _settingsService.Theme; + set => _settingsService.Theme = value; } - public bool IsDarkModeEnabled + public bool IsAutoUpdateEnabled { - get => _settingsService.IsDarkModeEnabled; - set => _settingsService.IsDarkModeEnabled = value; + get => _settingsService.IsAutoUpdateEnabled; + set => _settingsService.IsAutoUpdateEnabled = value; } public bool IsTokenPersisted diff --git a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs index c6e85eb..deced76 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 8cf0e9a..31b06db 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml @@ -24,6 +24,19 @@ BorderThickness="0,1"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <StackPanel Orientation="Vertical"> + <!-- Theme --> + <DockPanel + Margin="16,8" + LastChildFill="False" + ToolTip.Tip="Preferred user interface theme"> + <TextBlock DockPanel.Dock="Left" Text="Theme" /> + <ComboBox + Width="150" + DockPanel.Dock="Right" + ItemsSource="{Binding AvailableThemes}" + SelectedItem="{Binding Theme}" /> + </DockPanel> + <!-- Auto-updates --> <DockPanel Margin="16,8" @@ -35,19 +48,6 @@ <ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsAutoUpdateEnabled}" /> </DockPanel> - <!-- Dark mode --> - <DockPanel - Margin="16,8" - LastChildFill="False" - ToolTip.Tip="Use darker colors in the UI"> - <TextBlock DockPanel.Dock="Left" Text="Dark mode" /> - <ToggleSwitch - x:Name="DarkModeToggleSwitch" - DockPanel.Dock="Right" - IsChecked="{Binding IsDarkModeEnabled}" - IsCheckedChanged="DarkModeToggleSwitch_OnIsCheckedChanged" /> - </DockPanel> - <!-- Persist token --> <DockPanel Margin="16,8" diff --git a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs index 4eb9dc7..316aa8d 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs +++ b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs @@ -1,6 +1,4 @@ -using System.Windows; -using Avalonia.Interactivity; -using DiscordChatExporter.Gui.Framework; +using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.ViewModels.Dialogs; namespace DiscordChatExporter.Gui.Views.Dialogs; @@ -8,20 +6,4 @@ namespace DiscordChatExporter.Gui.Views.Dialogs; public partial class SettingsView : UserControl<SettingsViewModel> { 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(); - } - } }