Use a 3-way theme switcher instead of a 2-way switcher (#1233)

This commit is contained in:
Oleksii Holub 2024-05-13 23:56:21 +03:00 committed by GitHub
parent 9e7ad4d85c
commit 7a69c87b56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 106 additions and 116 deletions

View file

@ -5,7 +5,7 @@
<PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" /> <PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" /> <PackageReference Include="Gress" Version="2.1.1" />
<PackageReference Include="JsonExtensions" Version="1.2.0" /> <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="RazorBlade" Version="0.6.0" />
<PackageReference Include="Superpower" Version="3.0.0" /> <PackageReference Include="Superpower" Version="3.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.16.0" /> <PackageReference Include="WebMarkupMin.Core" Version="2.16.0" />

View file

@ -7,13 +7,18 @@
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles" xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
xmlns:materialControls="clr-namespace:Material.Styles.Controls;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: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> <Application.DataTemplates>
<framework:ViewManager /> <framework:ViewManager />
</Application.DataTemplates> </Application.DataTemplates>
<Application.Styles> <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 /> <materialIcons:MaterialIconStyles />
<dialogHostAvalonia:DialogHostStyles /> <dialogHostAvalonia:DialogHostStyles />

View file

@ -7,6 +7,8 @@ using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Services; using DiscordChatExporter.Gui.Services;
using DiscordChatExporter.Gui.Utils;
using DiscordChatExporter.Gui.Utils.Extensions;
using DiscordChatExporter.Gui.ViewModels; using DiscordChatExporter.Gui.ViewModels;
using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Components;
using DiscordChatExporter.Gui.ViewModels.Dialogs; using DiscordChatExporter.Gui.ViewModels.Dialogs;
@ -16,11 +18,14 @@ using Microsoft.Extensions.DependencyInjection;
namespace DiscordChatExporter.Gui; namespace DiscordChatExporter.Gui;
public partial class App : Application, IDisposable public class App : Application, IDisposable
{ {
private readonly ServiceProvider _services; private readonly ServiceProvider _services;
private readonly SettingsService _settingsService;
private readonly MainViewModel _mainViewModel; private readonly MainViewModel _mainViewModel;
private readonly DisposableCollector _eventRoot = new();
public App() public App()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
@ -43,17 +48,62 @@ public partial class App : Application, IDisposable
services.AddTransient<SettingsViewModel>(); services.AddTransient<SettingsViewModel>();
_services = services.BuildServiceProvider(true); _services = services.BuildServiceProvider(true);
_settingsService = _services.GetRequiredService<SettingsService>();
_mainViewModel = _services.GetRequiredService<ViewModelManager>().CreateMainViewModel(); _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() public override void Initialize()
{ {
base.Initialize();
// Increase maximum concurrent connections // Increase maximum concurrent connections
ServicePointManager.DefaultConnectionLimit = 20; ServicePointManager.DefaultConnectionLimit = 20;
AvaloniaXamlLoader.Load(this); 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() public override void OnFrameworkInitializationCompleted()
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@ -61,50 +111,20 @@ public partial class App : Application, IDisposable
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();
// Set custom theme colors // Set up custom theme colors
SetDefaultTheme(); 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 void Dispose()
{
public static void SetLightTheme()
{ {
if (Current is null) _eventRoot.Dispose();
return; _services.Dispose();
Current.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme = Theme.Create(
Theme.Light,
Color.Parse("#343838"),
Color.Parse("#F9A825")
);
}
public static void SetDarkTheme()
{
if (Current is null)
return;
Current.LocateMaterialTheme<MaterialThemeBase>().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();
} }
} }

View file

@ -11,7 +11,7 @@ public class LocaleToDisplayNameStringConverter : IValueConverter
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
value is string locale && !string.IsNullOrWhiteSpace(locale) value is string locale && !string.IsNullOrWhiteSpace(locale)
? CultureInfo.GetCultureInfo(locale).DisplayName ? CultureInfo.GetCultureInfo(locale).DisplayName
: "System default"; : "System";
public object ConvertBack( public object ConvertBack(
object? value, object? value,

View file

@ -22,7 +22,7 @@
<PackageReference Include="DialogHost.Avalonia" Version="0.7.7" /> <PackageReference Include="DialogHost.Avalonia" Version="0.7.7" />
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.4" PrivateAssets="all" /> <PackageReference Include="DotnetRuntimeBootstrapper" Version="2.5.4" PrivateAssets="all" />
<PackageReference Include="Gress" Version="2.1.1" /> <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="Material.Icons.Avalonia" Version="2.1.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Onova" Version="2.6.11" /> <PackageReference Include="Onova" Version="2.6.11" />

View file

@ -0,0 +1,8 @@
namespace DiscordChatExporter.Gui.Framework;
public enum ThemeVariant
{
System,
Light,
Dark
}

View file

@ -1,12 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using Avalonia;
using Avalonia.Platform;
using Cogwheel; using Cogwheel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Models; using DiscordChatExporter.Gui.Models;
using Microsoft.Win32;
namespace DiscordChatExporter.Gui.Services; namespace DiscordChatExporter.Gui.Services;
@ -18,10 +16,10 @@ public partial class SettingsService()
private bool _isUkraineSupportMessageEnabled = true; private bool _isUkraineSupportMessageEnabled = true;
[ObservableProperty] [ObservableProperty]
private bool _isAutoUpdateEnabled = true; private ThemeVariant _theme;
[ObservableProperty] [ObservableProperty]
private bool _isDarkModeEnabled; private bool _isAutoUpdateEnabled = true;
[ObservableProperty] [ObservableProperty]
private bool _isTokenPersisted = true; private bool _isTokenPersisted = true;
@ -62,17 +60,6 @@ public partial class SettingsService()
[ObservableProperty] [ObservableProperty]
private string? _lastAssetsDirPath; 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() public override void Save()
{ {
// Clear the token if it's not supposed to be persisted // Clear the token if it's not supposed to be persisted

View file

@ -1,5 +1,4 @@
using System; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.VisualTree; using Avalonia.VisualTree;

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Core.Utils.Extensions; using DiscordChatExporter.Core.Utils.Extensions;
using DiscordChatExporter.Gui.Framework; using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Models; using DiscordChatExporter.Gui.Models;
@ -23,18 +22,20 @@ public class SettingsViewModel : DialogViewModelBase
_eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged)); _eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged));
} }
public IReadOnlyList<ThemeVariant> AvailableThemes { get; } = Enum.GetValues<ThemeVariant>();
public ThemeVariant Theme
{
get => _settingsService.Theme;
set => _settingsService.Theme = value;
}
public bool IsAutoUpdateEnabled public bool IsAutoUpdateEnabled
{ {
get => _settingsService.IsAutoUpdateEnabled; get => _settingsService.IsAutoUpdateEnabled;
set => _settingsService.IsAutoUpdateEnabled = value; set => _settingsService.IsAutoUpdateEnabled = value;
} }
public bool IsDarkModeEnabled
{
get => _settingsService.IsDarkModeEnabled;
set => _settingsService.IsDarkModeEnabled = value;
}
public bool IsTokenPersisted public bool IsTokenPersisted
{ {
get => _settingsService.IsTokenPersisted; get => _settingsService.IsTokenPersisted;

View file

@ -79,18 +79,6 @@ public partial class MainViewModel(
[RelayCommand] [RelayCommand]
private async Task InitializeAsync() 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 ShowUkraineSupportMessageAsync();
await CheckForUpdatesAsync(); await CheckForUpdatesAsync();
} }

View file

@ -24,6 +24,19 @@
BorderThickness="0,1"> BorderThickness="0,1">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"> <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Orientation="Vertical"> <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 --> <!-- Auto-updates -->
<DockPanel <DockPanel
Margin="16,8" Margin="16,8"
@ -35,19 +48,6 @@
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsAutoUpdateEnabled}" /> <ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding IsAutoUpdateEnabled}" />
</DockPanel> </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 --> <!-- Persist token -->
<DockPanel <DockPanel
Margin="16,8" Margin="16,8"

View file

@ -1,6 +1,4 @@
using System.Windows; using DiscordChatExporter.Gui.Framework;
using Avalonia.Interactivity;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.ViewModels.Dialogs; using DiscordChatExporter.Gui.ViewModels.Dialogs;
namespace DiscordChatExporter.Gui.Views.Dialogs; namespace DiscordChatExporter.Gui.Views.Dialogs;
@ -8,20 +6,4 @@ namespace DiscordChatExporter.Gui.Views.Dialogs;
public partial class SettingsView : UserControl<SettingsViewModel> public partial class SettingsView : UserControl<SettingsViewModel>
{ {
public SettingsView() => InitializeComponent(); 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();
}
}
} }