diff --git a/CHANGELOG.md b/CHANGELOG.md index 48e9ded1e..b71ef00af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Add `System` option to **Settings** > **UI** > **Theme** + - Follows the light or dark preference of the browser that is viewing ErsatzTV, and updates without a refresh when that preference changes + ### Fixed - Fix regression from `v26.2.0` that caused channel logo watermarks to be ignored when the logo is a url - This affected external logo urls and generated channel logos diff --git a/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs b/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs index 5cd471d4c..21a3b0389 100644 --- a/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs +++ b/ErsatzTV.Application/Configuration/Commands/UpdateUiSettingsHandler.cs @@ -17,10 +17,13 @@ public class UpdateUiSettingsHandler(IConfigElementRepository configElementRepos private async Task ApplyUpdate(UiSettingsViewModel uiSettings, CancellationToken cancellationToken) { await configElementRepository.Upsert( - ConfigElementKey.PagesIsDarkMode, - uiSettings.IsDarkMode, + ConfigElementKey.PagesThemeMode, + uiSettings.ThemeMode, cancellationToken); + // the legacy dark mode flag is only used to migrate the theme mode setting + await configElementRepository.Delete(ConfigElementKey.PagesIsDarkMode, cancellationToken); + await configElementRepository.Upsert(ConfigElementKey.PagesLanguage, uiSettings.Language, cancellationToken); return Unit.Default; diff --git a/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs b/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs index ce07bc0c0..ae22efd95 100644 --- a/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs +++ b/ErsatzTV.Application/Configuration/Queries/GetUiSettingsHandler.cs @@ -8,6 +8,11 @@ public class GetUiSettingsHandler(IConfigElementRepository configElementReposito { public async Task Handle(GetUiSettings request, CancellationToken cancellationToken) { + Option pagesThemeMode = await configElementRepository.GetValue( + ConfigElementKey.PagesThemeMode, + cancellationToken); + + // fall back to the legacy dark mode flag for installs that predate the theme mode setting Option pagesIsDarkMode = await configElementRepository.GetValue( ConfigElementKey.PagesIsDarkMode, cancellationToken); @@ -18,7 +23,10 @@ public class GetUiSettingsHandler(IConfigElementRepository configElementReposito return new UiSettingsViewModel { - IsDarkMode = await pagesIsDarkMode.IfNoneAsync(true), + ThemeMode = pagesThemeMode.IfNone( + () => pagesIsDarkMode.Match( + isDarkMode => isDarkMode ? ThemeMode.Dark : ThemeMode.Light, + () => ThemeMode.Dark)), Language = await pagesLanguage.IfNoneAsync("en") }; } diff --git a/ErsatzTV.Application/Configuration/ThemeMode.cs b/ErsatzTV.Application/Configuration/ThemeMode.cs new file mode 100644 index 000000000..8e1e051aa --- /dev/null +++ b/ErsatzTV.Application/Configuration/ThemeMode.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Application.Configuration; + +public enum ThemeMode +{ + Dark = 0, + Light = 1, + System = 2 +} diff --git a/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs b/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs index bfa3bab24..7ab244d73 100644 --- a/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs +++ b/ErsatzTV.Application/Configuration/UiSettingsViewModel.cs @@ -2,7 +2,7 @@ namespace ErsatzTV.Application.Configuration; public class UiSettingsViewModel { - public bool IsDarkMode { get; set; } + public ThemeMode ThemeMode { get; set; } public string Language { get; set; } } diff --git a/ErsatzTV.Core/Domain/ConfigElementKey.cs b/ErsatzTV.Core/Domain/ConfigElementKey.cs index 181aa8a99..becfa6b76 100644 --- a/ErsatzTV.Core/Domain/ConfigElementKey.cs +++ b/ErsatzTV.Core/Domain/ConfigElementKey.cs @@ -32,6 +32,7 @@ public class ConfigElementKey public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count"); public static ConfigElementKey HDHRUUID => new("hdhr.uuid"); public static ConfigElementKey PagesIsDarkMode => new("pages.is_dark_mode"); + public static ConfigElementKey PagesThemeMode => new("pages.theme_mode"); public static ConfigElementKey PagesLanguage => new("pages.language"); public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size"); public static ConfigElementKey ChannelsShowDisabled => new("pages.channels.show_disabled"); diff --git a/ErsatzTV/Pages/Settings/UISettings.razor b/ErsatzTV/Pages/Settings/UISettings.razor index 9815d0c1c..abb7dbca6 100644 --- a/ErsatzTV/Pages/Settings/UISettings.razor +++ b/ErsatzTV/Pages/Settings/UISettings.razor @@ -19,9 +19,10 @@
Theme
- - Dark - Light + + Dark + Light + System diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index c4e280f90..82fbb594f 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -17,7 +17,7 @@ @inject IHealthCheckService HealthCheckService; @inject IStringLocalizer StringLocalizer; - + @@ -279,7 +279,10 @@ private int _playoutWarnings; private int _errors; private int _warnings; + private MudThemeProvider _mudThemeProvider; + private ThemeMode _themeMode = ThemeMode.Dark; private bool _isDarkMode = true; + private bool _systemIsDarkMode = true; protected override void OnInitialized() { @@ -298,6 +301,45 @@ await HandleHealthCheckSummary(HealthCheckService.GetHealthCheckSummary(), CancellationToken.None); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (!firstRender || _mudThemeProvider is null) + { + return; + } + + // the browser's color scheme is only available once we can use js interop + _systemIsDarkMode = await _mudThemeProvider.GetSystemDarkModeAsync(); + if (_themeMode is ThemeMode.System) + { + ApplyThemeMode(); + StateHasChanged(); + } + + await _mudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged); + } + + private async Task OnSystemDarkModeChanged(bool isDarkMode) + { + _systemIsDarkMode = isDarkMode; + + if (_themeMode is ThemeMode.System) + { + ApplyThemeMode(); + await InvokeAsync(StateHasChanged); + } + } + + private void ApplyThemeMode() => + _isDarkMode = _themeMode switch + { + ThemeMode.Light => false, + ThemeMode.System => _systemIsDarkMode, + _ => true + }; + public void Dispose() { SystemStartup.OnDatabaseReady -= OnStartupProgress; @@ -415,9 +457,9 @@ { _searchTargets ??= await Mediator.Send(new QuerySearchTargets(), token); - _isDarkMode = await Mediator.Send(new GetConfigElementByKey(ConfigElementKey.PagesIsDarkMode), token) - .MapT(result => !bool.TryParse(result.Value, out bool value) || value) - .IfNoneAsync(true); + UiSettingsViewModel uiSettings = await Mediator.Send(new GetUiSettings(), token); + _themeMode = uiSettings.ThemeMode; + ApplyThemeMode(); _playoutWarnings = await Mediator.Send(new GetPlayoutWarningsCount(), token); }