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();
- }
- }
}