diff --git a/LightBulb.Core.Tests/LightBulb.Core.Tests.csproj b/LightBulb.Core.Tests/LightBulb.Core.Tests.csproj
index b829b6de..ba4c9c8f 100644
--- a/LightBulb.Core.Tests/LightBulb.Core.Tests.csproj
+++ b/LightBulb.Core.Tests/LightBulb.Core.Tests.csproj
@@ -11,12 +11,12 @@
-
+
-
-
+
+
diff --git a/LightBulb.Core/LightBulb.Core.csproj b/LightBulb.Core/LightBulb.Core.csproj
index 9cb25d06..35a9f206 100644
--- a/LightBulb.Core/LightBulb.Core.csproj
+++ b/LightBulb.Core/LightBulb.Core.csproj
@@ -1,7 +1,7 @@
-
+
diff --git a/LightBulb.PlatformInterop/LightBulb.PlatformInterop.csproj b/LightBulb.PlatformInterop/LightBulb.PlatformInterop.csproj
index 5e9e81f4..a37e6f7e 100644
--- a/LightBulb.PlatformInterop/LightBulb.PlatformInterop.csproj
+++ b/LightBulb.PlatformInterop/LightBulb.PlatformInterop.csproj
@@ -1,7 +1,7 @@
-
+
\ No newline at end of file
diff --git a/LightBulb.PlatformInterop/Timer.cs b/LightBulb.PlatformInterop/Timer.cs
index 096a71f9..34a25a2e 100644
--- a/LightBulb.PlatformInterop/Timer.cs
+++ b/LightBulb.PlatformInterop/Timer.cs
@@ -33,6 +33,8 @@ public Timer(TimeSpan firstTickDelay, TimeSpan interval, Action tick)
public Timer(TimeSpan interval, Action callback)
: this(TimeSpan.Zero, interval, callback) { }
+ public TimeSpan Interval => _interval;
+
private void Tick()
{
// Prevent multiple reentry
diff --git a/LightBulb/App.axaml b/LightBulb/App.axaml
index bcb2afd9..820c0ee1 100644
--- a/LightBulb/App.axaml
+++ b/LightBulb/App.axaml
@@ -6,16 +6,40 @@
xmlns:framework="clr-namespace:LightBulb.Framework"
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;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">
-
-
+
+
+
+
+
+
+
+
+
+
@@ -95,25 +119,36 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
diff --git a/LightBulb/App.axaml.cs b/LightBulb/App.axaml.cs
index a9dbce0a..4adbe507 100644
--- a/LightBulb/App.axaml.cs
+++ b/LightBulb/App.axaml.cs
@@ -5,6 +5,7 @@
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
+using Avalonia.Platform;
using Avalonia.Threading;
using LightBulb.Framework;
using LightBulb.Services;
@@ -25,6 +26,7 @@ public class App : Application, IDisposable
private readonly DisposableCollector _eventRoot = new();
private readonly ServiceProvider _services;
+ private readonly SettingsService _settingsService;
private readonly MainViewModel _mainViewModel;
public App()
@@ -55,14 +57,29 @@ public App()
services.AddTransient();
_services = services.BuildServiceProvider(true);
+ _settingsService = _services.GetRequiredService();
_mainViewModel = _services.GetRequiredService().CreateMainViewModel();
- }
- public override void Initialize()
- {
- AvaloniaXamlLoader.Load(this);
+ // Re-initialize the theme when the user changes it
+ _eventRoot.Add(
+ _settingsService.WatchProperty(
+ o => o.Theme,
+ () =>
+ {
+ RequestedThemeVariant = _settingsService.Theme switch
+ {
+ ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light,
+ ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark,
+ _ => Avalonia.Styling.ThemeVariant.Default
+ };
+
+ InitializeTheme();
+ },
+ false
+ )
+ );
- // Tray icon does not support binding so we use this hack to update its tooltip
+ // Tray icon does not support binding so we use this hack to synchronize its tooltip
_eventRoot.Add(
_mainViewModel.Dashboard.WatchProperties(
[o => o.IsActive, o => o.CurrentConfiguration],
@@ -83,11 +100,34 @@ public override void Initialize()
if (TrayIcon.GetIcons(this)?.FirstOrDefault() is { } trayIcon)
trayIcon.ToolTipText = tooltip;
});
- }
+ },
+ false
)
);
}
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ private void InitializeTheme()
+ {
+ var actualTheme = RequestedThemeVariant?.Key switch
+ {
+ "Light" => PlatformThemeVariant.Light,
+ "Dark" => PlatformThemeVariant.Dark,
+ _ => PlatformSettings?.GetColorValues().ThemeVariant ?? PlatformThemeVariant.Light
+ };
+
+ this.LocateMaterialTheme().CurrentTheme =
+ actualTheme == PlatformThemeVariant.Light
+ ? Theme.Create(Theme.Light, Color.Parse("#343838"), Color.Parse("#F9A825"))
+ : Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825"));
+ }
+
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
@@ -95,14 +135,17 @@ public override void OnFrameworkInitializationCompleted()
base.OnFrameworkInitializationCompleted();
- // Set custom theme colors
- this.LocateMaterialTheme().CurrentTheme = Theme.Create(
- Theme.Light,
- Color.Parse("#343838"),
- Color.Parse("#F9A825")
- );
+ // Set up custom theme colors
+ InitializeTheme();
+
+ // Load settings
+ _settingsService.Load();
}
+ private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) =>
+ // Re-initialize the theme when the system theme changes
+ InitializeTheme();
+
private void TrayIcon_OnClicked(object? sender, EventArgs args) => this.TryFocusMainWindow();
private void ShowSettingsMenuItem_OnClick(object? sender, EventArgs args)
diff --git a/LightBulb/Framework/ThemeVariant.cs b/LightBulb/Framework/ThemeVariant.cs
new file mode 100644
index 00000000..4a062ad7
--- /dev/null
+++ b/LightBulb/Framework/ThemeVariant.cs
@@ -0,0 +1,8 @@
+namespace LightBulb.Framework;
+
+public enum ThemeVariant
+{
+ System,
+ Light,
+ Dark
+}
diff --git a/LightBulb/LightBulb.csproj b/LightBulb/LightBulb.csproj
index 381ba273..63a90794 100644
--- a/LightBulb/LightBulb.csproj
+++ b/LightBulb/LightBulb.csproj
@@ -3,7 +3,6 @@
WinExe
true
- app.manifest
..\favicon.ico
@@ -20,10 +19,10 @@
-
+
-
+
diff --git a/LightBulb/Services/SettingsService.cs b/LightBulb/Services/SettingsService.cs
index 33e40d99..e85a80b2 100644
--- a/LightBulb/Services/SettingsService.cs
+++ b/LightBulb/Services/SettingsService.cs
@@ -5,6 +5,7 @@
using Cogwheel;
using CommunityToolkit.Mvvm.ComponentModel;
using LightBulb.Core;
+using LightBulb.Framework;
using LightBulb.Models;
using LightBulb.PlatformInterop;
using LightBulb.Utils;
@@ -63,6 +64,9 @@ public partial class SettingsService() : SettingsBase(GetFilePath())
[ObservableProperty]
private double _configurationTransitionOffset;
+ [ObservableProperty]
+ private TimeSpan _configurationSmoothingMaxDuration = TimeSpan.FromSeconds(5);
+
// Location
[ObservableProperty]
@@ -81,6 +85,9 @@ public partial class SettingsService() : SettingsBase(GetFilePath())
// Advanced
+ [ObservableProperty]
+ private ThemeVariant _theme;
+
[ObservableProperty]
[property: JsonIgnore] // comes from registry
private bool _isAutoStartEnabled;
diff --git a/LightBulb/ViewModels/Components/DashboardViewModel.cs b/LightBulb/ViewModels/Components/DashboardViewModel.cs
index 89404024..22f9f9fd 100644
--- a/LightBulb/ViewModels/Components/DashboardViewModel.cs
+++ b/LightBulb/ViewModels/Components/DashboardViewModel.cs
@@ -69,6 +69,9 @@ public partial class DashboardViewModel : ViewModelBase
[ObservableProperty]
private ColorConfiguration _currentConfiguration = ColorConfiguration.Default;
+ private ColorConfiguration? _configurationSmoothingSource;
+ private ColorConfiguration? _configurationSmoothingTarget;
+
public DashboardViewModel(
SettingsService settingsService,
GammaService gammaService,
@@ -287,11 +290,60 @@ private void UpdateInstant()
private void UpdateConfiguration()
{
- var isSmooth = _settingsService.IsConfigurationSmoothingEnabled && !IsCyclePreviewEnabled;
+ var isSmooth =
+ !IsCyclePreviewEnabled
+ && CurrentConfiguration != TargetConfiguration
+ && _settingsService.IsConfigurationSmoothingEnabled
+ && _settingsService.ConfigurationSmoothingMaxDuration.TotalSeconds >= 0.1;
+
+ if (isSmooth)
+ {
+ // Check if the target configuration has changed since the last transition started
+ if (
+ _configurationSmoothingTarget != TargetConfiguration
+ || _configurationSmoothingSource is null
+ )
+ {
+ _configurationSmoothingSource = CurrentConfiguration;
+ _configurationSmoothingTarget = TargetConfiguration;
+ }
+
+ var brightnessDelta = Math.Abs(
+ _configurationSmoothingTarget.Value.Brightness
+ - _configurationSmoothingSource.Value.Brightness
+ );
+
+ var brightnessStep = Math.Max(
+ brightnessDelta
+ / _settingsService.ConfigurationSmoothingMaxDuration.TotalSeconds
+ * _updateConfigurationTimer.Interval.TotalSeconds,
+ 0.08
+ );
+
+ var temperatureDelta = Math.Abs(
+ _configurationSmoothingTarget.Value.Temperature
+ - _configurationSmoothingSource.Value.Temperature
+ );
+
+ var temperatureStep = Math.Max(
+ temperatureDelta
+ / _settingsService.ConfigurationSmoothingMaxDuration.TotalSeconds
+ * _updateConfigurationTimer.Interval.TotalSeconds,
+ 30
+ );
- CurrentConfiguration = isSmooth
- ? CurrentConfiguration.StepTo(TargetConfiguration, 30, 0.008)
- : TargetConfiguration;
+ CurrentConfiguration = CurrentConfiguration.StepTo(
+ TargetConfiguration,
+ temperatureStep,
+ brightnessStep
+ );
+ }
+ else
+ {
+ CurrentConfiguration = TargetConfiguration;
+ _configurationSmoothingSource = null;
+ _configurationSmoothingTarget = null;
+ }
_gammaService.SetGamma(CurrentConfiguration);
}
diff --git a/LightBulb/ViewModels/Components/Settings/AdvancedSettingsTabViewModel.cs b/LightBulb/ViewModels/Components/Settings/AdvancedSettingsTabViewModel.cs
index 4f8b2d04..7cb85f9d 100644
--- a/LightBulb/ViewModels/Components/Settings/AdvancedSettingsTabViewModel.cs
+++ b/LightBulb/ViewModels/Components/Settings/AdvancedSettingsTabViewModel.cs
@@ -1,10 +1,21 @@
-using LightBulb.Services;
+using System;
+using System.Collections.Generic;
+using LightBulb.Framework;
+using LightBulb.Services;
namespace LightBulb.ViewModels.Components.Settings;
public class AdvancedSettingsTabViewModel(SettingsService settingsService)
: SettingsTabViewModelBase(settingsService, 2, "Advanced")
{
+ public IReadOnlyList AvailableThemes { get; } = Enum.GetValues();
+
+ public ThemeVariant Theme
+ {
+ get => SettingsService.Theme;
+ set => SettingsService.Theme = value;
+ }
+
public bool IsAutoStartEnabled
{
get => SettingsService.IsAutoStartEnabled;
diff --git a/LightBulb/ViewModels/MainViewModel.cs b/LightBulb/ViewModels/MainViewModel.cs
index 39685004..531e8994 100644
--- a/LightBulb/ViewModels/MainViewModel.cs
+++ b/LightBulb/ViewModels/MainViewModel.cs
@@ -143,9 +143,6 @@ Click LEARN MORE to find ways that you can help.
[RelayCommand]
private async Task InitializeAsync()
{
- // Load settings
- settingsService.Load();
-
await FinalizePendingUpdateAsync();
await ShowGammaRangePromptAsync();
await ShowFirstTimeExperienceMessageAsync();
diff --git a/LightBulb/Views/Components/DashboardView.axaml b/LightBulb/Views/Components/DashboardView.axaml
index 989f41f6..349bab77 100644
--- a/LightBulb/Views/Components/DashboardView.axaml
+++ b/LightBulb/Views/Components/DashboardView.axaml
@@ -35,20 +35,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -57,14 +88,14 @@
Height="180"
EndAngle="{Binding SunsetStart, Converter={x:Static converters:TimeOnlyToDegreesDoubleConverter.Instance}}"
StartAngle="{Binding SunriseEnd, Converter={x:Static converters:TimeOnlyToDegreesDoubleConverter.Instance}}"
- Stroke="#8AC0FF"
+ Stroke="{DynamicResource SundialDayBorderBrush}"
StrokeThickness="28" />
@@ -73,14 +104,14 @@
Height="180"
EndAngle="{Binding SunriseEnd, Converter={x:Static converters:TimeOnlyToDegreesDoubleConverter.Instance}}"
StartAngle="{Binding SunriseStart, Converter={x:Static converters:TimeOnlyToDegreesDoubleConverter.Instance}}"
- Stroke="#FFC766"
+ Stroke="{DynamicResource SundialSunriseBorderBrush}"
StrokeThickness="28" />
@@ -89,14 +120,14 @@
Height="180"
EndAngle="{Binding SunsetEnd, Converter={x:Static converters:TimeOnlyToDegreesDoubleConverter.Instance}}"
StartAngle="{Binding SunsetStart, Converter={x:Static converters:TimeOnlyToDegreesDoubleConverter.Instance}}"
- Stroke="#FFC766"
+ Stroke="{DynamicResource SundialSunsetBorderBrush}"
StrokeThickness="28" />
@@ -104,13 +135,13 @@
Width="180"
Height="180"
Angle="{Binding Instant.TimeOfDay.TotalDays, Converter={x:Static converters:FractionToDegreesConverter.Instance}}"
- Fill="#FF7733"
+ Fill="{DynamicResource SundialMarkerBorderBrush}"
Size="28" />
diff --git a/LightBulb/Views/Components/Settings/AdvancedSettingsTabView.axaml b/LightBulb/Views/Components/Settings/AdvancedSettingsTabView.axaml
index b897a8cb..f3e8cdd3 100644
--- a/LightBulb/Views/Components/Settings/AdvancedSettingsTabView.axaml
+++ b/LightBulb/Views/Components/Settings/AdvancedSettingsTabView.axaml
@@ -8,82 +8,67 @@
-
-
-
-
+
+
+
+ ItemsSource="{Binding AvailableThemes}"
+ SelectedItem="{Binding Theme}" />
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
\ No newline at end of file
diff --git a/LightBulb/Views/Dialogs/SettingsView.axaml b/LightBulb/Views/Dialogs/SettingsView.axaml
index 94123461..05facd5b 100644
--- a/LightBulb/Views/Dialogs/SettingsView.axaml
+++ b/LightBulb/Views/Dialogs/SettingsView.axaml
@@ -16,8 +16,8 @@
-
+ Background="{DynamicResource MaterialDarkBackgroundBrush}">
+
@@ -45,11 +45,13 @@
-
+
diff --git a/LightBulb/Views/MainView.axaml b/LightBulb/Views/MainView.axaml
index d896487b..c137fd97 100644
--- a/LightBulb/Views/MainView.axaml
+++ b/LightBulb/Views/MainView.axaml
@@ -21,7 +21,10 @@
-
+
@@ -43,6 +46,7 @@
Padding="0"
VerticalAlignment="Center"
Background="{DynamicResource MaterialSecondaryMidBrush}"
+ Foreground="{DynamicResource MaterialSecondaryMidForegroundBrush}"
IsChecked="{Binding Dashboard.IsEnabled}"
Theme="{DynamicResource MaterialIconToggleButton}"
ToolTip.Tip="Toggle LightBulb on/off">
@@ -67,7 +71,7 @@
Margin="8,1,0,0"
VerticalAlignment="Center"
FontSize="16"
- Foreground="{DynamicResource MaterialPrimaryMidForegroundBrush}">
+ Foreground="{DynamicResource MaterialDarkForegroundBrush}">
-
-
-
-
-
-
-
-
\ No newline at end of file