Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DiscordChatExporter.Core/DiscordChatExporter.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
9 changes: 7 additions & 2 deletions DiscordChatExporter.Gui/App.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 />

Expand Down
102 changes: 61 additions & 41 deletions DiscordChatExporter.Gui/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
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;
Expand All @@ -16,11 +18,14 @@

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();
Expand All @@ -43,68 +48,83 @@ public App()
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)
desktop.MainWindow = new MainView { DataContext = _mainViewModel };

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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
8 changes: 8 additions & 0 deletions DiscordChatExporter.Gui/Framework/ThemeVariant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace DiscordChatExporter.Gui.Framework;

public enum ThemeVariant
{
System,
Light,
Dark
}
19 changes: 3 additions & 16 deletions DiscordChatExporter.Gui/Services/SettingsService.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.VisualTree;

Expand Down
15 changes: 8 additions & 7 deletions DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,16 +22,18 @@ public SettingsViewModel(SettingsService settingsService)
_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
Expand Down
12 changes: 0 additions & 12 deletions DiscordChatExporter.Gui/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,18 +79,6 @@ private async Task CheckForUpdatesAsync()
[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();
}
Expand Down
26 changes: 13 additions & 13 deletions DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
20 changes: 1 addition & 19 deletions DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,9 @@
using System.Windows;
using Avalonia.Interactivity;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.ViewModels.Dialogs;

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