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="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" />

View file

@ -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 />

View file

@ -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();
// 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<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();
_eventRoot.Dispose();
_services.Dispose();
}
}

View file

@ -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,

View file

@ -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" />

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.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

View file

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

View file

@ -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<ThemeVariant> AvailableThemes { get; } = Enum.GetValues<ThemeVariant>();
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;

View file

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

View file

@ -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"

View file

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