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